{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "hUCaGdAj9-9F"
   },
   "source": [
    "# 使用 LangChain 在 HuggingFace 文档上构建高级 RAG\n",
    "_作者: [Aymeric Roucher](https://huggingface.co/m-ric)_"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "DKv51c_h9-9H"
   },
   "source": [
    "这个 notebook 主要讲述了你怎么构建一个高级的 RAG，用于回答一个关于特定知识库的问题（这里，是 HuggingFace 文档），使用 LangChain。\n",
    "\n",
    "对于 RAG 的介绍，你可以查看[这个教程](rag_zephyr_langch)\n",
    "\n",
    "RAG 系统是复杂的，它有许多组块:这里画一个简单的 RAG 图表，其中用蓝色标注了所有系统增强的可能性。\n",
    "\n",
    "<img src=\"https://huggingface.co/datasets/huggingface/cookbook-images/resolve/main/RAG_workflow.png\" height=\"700\">\n",
    "\n",
    "> 💡 可以看到，这个架构中有许多步骤可以调整：正确调整系统将带来显著的性能提升。\n",
    "\n",
    "在这个 notebook 中，我们将研究许多这些蓝色标注的部分，看看如何调整你的 RAG 系统以获得最佳性能。\n",
    "\n",
    "__让我们深入研究模型架构吧！__ 首先，安装所需的模型依赖项。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "NSX0p0rV9-9I"
   },
   "outputs": [],
   "source": [
    "!pip install -q torch transformers transformers accelerate bitsandbytes langchain sentence-transformers faiss-gpu openpyxl pacmap datasets langchain-community ragatouille"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "8_Uyukt39-9J"
   },
   "outputs": [],
   "source": [
    "%reload_ext dotenv\n",
    "%dotenv"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "eoujYMwW9-9J"
   },
   "outputs": [],
   "source": [
    "from tqdm.notebook import tqdm\n",
    "import pandas as pd\n",
    "from typing import Optional, List, Tuple\n",
    "from datasets import Dataset\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "pd.set_option(\n",
    "    \"display.max_colwidth\", None\n",
    ")  # this will be helpful when visualizing retriever outputs"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "Kr6rN10U9-9J"
   },
   "source": [
    "### 加载你的知识基础"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "qZLVIEVW9-9J"
   },
   "outputs": [],
   "source": [
    "import datasets\n",
    "\n",
    "ds = datasets.load_dataset(\"m-ric/huggingface_doc\", split=\"train\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "836Q7vF49-9K"
   },
   "outputs": [],
   "source": [
    "from langchain.docstore.document import Document as LangchainDocument\n",
    "\n",
    "RAW_KNOWLEDGE_BASE = [\n",
    "    LangchainDocument(page_content=doc[\"text\"], metadata={\"source\": doc[\"source\"]})\n",
    "    for doc in tqdm(ds)\n",
    "]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "0_LxjD5h9-9K"
   },
   "source": [
    "# 1. 检索器- 嵌入 🗂️\n",
    "__检索器的作用类似于内部搜索引擎__：给定用户查询，它从你的知识库中返回几个相关的片段。\n",
    "\n",
    "这些片段随后将被输入到阅读器模型中，以帮助其生成答案。\n",
    "\n",
    "所以 __我们的目标在这里是，给定一个用户问题，从我们的知识库中找到最多的片段来回答这个问题。__\n",
    "\n",
    "这是一个宽泛的目标，它留下了一些问题。我们应该检索多少片段？这个参数将被命名为`top_k`。\n",
    "\n",
    "这些片段应该有多长？这被称为 `chunk size` （片段大小）。没有一刀切的答案，但这里有一些要点：\n",
    "- 🔀 你的 `chunk size` 允许从一段片段到另一段片段有所不同。\n",
    "- 由于你的检索中总会存在一些噪音，增加 `top_k` 可以提高你检索到的片段中包含相关元素的概率。🎯 射更多的箭增加了你命中目标的概率。\n",
    "- 同时，你检索到的文档的总长度不应过高：例如，对于大多数当前模型来说，16k 个 token 可能会因为[中间丢失现象](https://huggingface.co/papers/2307.03172)而在信息中淹没你的阅读器模型。🎯 只给你的阅读器模型提供最相关的见解，而不是一堆书！\n",
    "\n",
    "\n",
    "> 在这个 notebook 中，我们使用 Langchain 库，因为 __它为向量数据库提供了大量的选项，并允许我们在整个处理过程中保留文档的元数据__。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "-uS6Mv8O9-9L"
   },
   "source": [
    "### 1.1 将文档拆分为片段(chuncks)\n",
    "\n",
    "- 在这一部分，__我们将知识库中的文档拆分成更小的片段__，这些片段将是喂给阅读器 LLM 生成答案的片段。\n",
    "- 目标是准备一组**语义上相关的片段**。因此，它们的大小应该适配确切的想法：太小会截断想法，太大则会稀释它们。\n",
    "\n",
    "💡 _对于文本拆分存在许多选项：按单词拆分，按句子边界拆分，递归拆分以树状方式处理文档以保留结构信息... 要了解更多关于拆分的信息，我建议你阅读[这个很棒的 notebook](https://github.com/FullStackRetrieval-com/RetrievalTutorials/blob/main/5_Levels_Of_Text_Splitting.ipynb)，这是由 Greg Kamradt 编写的。_\n",
    "\n",
    "\n",
    "- **递归拆分**使用给定的一组分隔符逐步将文本分解为更小的部分，这些分隔符按从最重要到最不重要的顺序排序。如果第一次拆分没有给出正确大小或形状的片段，该方法会使用不同的分隔符在新的片段上重复自身。例如，使用分隔符列表`[\"\\n\\n\", \"\\n\", \".\", \"\"]`：\n",
    "    - 该方法首先在出现双行中断`\"\\n\\n\"`的任何地方拆分文档。\n",
    "    - 结果文档将在简单的行中断`\"\\n\"`处再次拆分，然后在句子结尾`\".\"`处拆分。\n",
    "    - 最后，如果有些片段仍然太大，它们将在超过最大大小时拆分。\n",
    "\n",
    "- 使用这种方法，整体结构得到了很好的保留，代价是片段大小会有轻微的变化。\n",
    "\n",
    "> [这个空间](https://huggingface.co/spaces/A-Roucher/chunk_visualizer)让你可视化不同的拆分选项如何影响你得到的片段。\n",
    "\n",
    "🔬 让我们用片段大小做一些实验，从任意大小开始，看看拆分是如何工作的。我们使用 Langchain 的 `RecursiveCharacterTextSplitter` 实现递归拆分。\n",
    "- 参数 `chunk_size` 控制单个片段的长度：这个长度默认计算为片段中的字符数。\n",
    "- 参数 `chunk_overlap` 允许相邻片段彼此有一些重叠。这减少了想法被两个相邻片段之间的拆分切割成两半的概率。我们武断地将这个设置为片段大小的1/10，你可以尝试不同的值！"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "M4m6TwDJ9-9L"
   },
   "outputs": [],
   "source": [
    "from langchain.text_splitter import RecursiveCharacterTextSplitter\n",
    "\n",
    "# We use a hierarchical list of separators specifically tailored for splitting Markdown documents\n",
    "# This list is taken from LangChain's MarkdownTextSplitter class.\n",
    "MARKDOWN_SEPARATORS = [\n",
    "    \"\\n#{1,6} \",\n",
    "    \"```\\n\",\n",
    "    \"\\n\\\\*\\\\*\\\\*+\\n\",\n",
    "    \"\\n---+\\n\",\n",
    "    \"\\n___+\\n\",\n",
    "    \"\\n\\n\",\n",
    "    \"\\n\",\n",
    "    \" \",\n",
    "    \"\",\n",
    "]\n",
    "\n",
    "text_splitter = RecursiveCharacterTextSplitter(\n",
    "    chunk_size=1000,  # the maximum number of characters in a chunk: we selected this value arbitrarily\n",
    "    chunk_overlap=100,  # the number of characters to overlap between chunks\n",
    "    add_start_index=True,  # If `True`, includes chunk's start index in metadata\n",
    "    strip_whitespace=True,  # If `True`, strips whitespace from the start and end of every document\n",
    "    separators=MARKDOWN_SEPARATORS,\n",
    ")\n",
    "\n",
    "docs_processed = []\n",
    "for doc in RAW_KNOWLEDGE_BASE:\n",
    "    docs_processed += text_splitter.split_documents([doc])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "d5jJUMgb9-9M"
   },
   "source": [
    "我们还必须记住，当我们嵌入文档时，我们将使用一个接受特定最大序列长度 `max_seq_length` 的嵌入模型。\n",
    "\n",
    "因此，我们应该确保我们的片段大小低于这个限制，因为任何更长的片段在处理之前都会被截断，从而失去相关性。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "referenced_widgets": [
      "ae043feeb0914c879e2a9008b413d952"
     ]
    },
    "id": "B4hoki349-9M",
    "outputId": "64f92a61-7839-476d-f456-7eefde04c20b"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Model's maximum sequence length: 512\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "ae043feeb0914c879e2a9008b413d952",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "  0%|          | 0/31085 [00:00<?, ?it/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAo4AAAGzCAYAAAChApYOAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAABUuElEQVR4nO3deVwV9eL/8TfIrgKiCaKoXC33LSzFvURwTcsl09JM85Z60ywtK82lcitzTfN20xa9llbmtVJxKTXJLXFLzcqyNKBExBURPr8/+p35egR0UDhAvp6Ph486n/mcz3zmM3Nm3mdmzuBmjDECAAAArsG9oDsAAACAooHgCAAAAFsIjgAAALCF4AgAAABbCI4AAACwheAIAAAAWwiOAAAAsIXgCAAAAFsIjgAAALAl34Pj2LFj5ebmlt+zkSS1atVKrVq1sl5/+eWXcnNz07Jly1wy/4cffliVK1d2ybyu15kzZzRgwACFhITIzc1Nw4YNy3Ubbm5uGjt2bJ737WZUuXJlPfzwwwXdjWt6+OGHVaJEiXydh6u2K1ftF1y9/7lRP//8s9zc3LRw4cI8a3PhwoVyc3PTzz//nGdt2lW5cmV17NjR5fO9UWfOnFHZsmW1aNEiq8yVx9G/u7w4Btrl2P537NiRb/O4Xj179lSPHj2u6725Co6OQXD88/HxUWhoqGJiYjRz5kydPn36ujpxpePHj2vs2LGKj4/Pk/byUmHumx2vvPKKFi5cqMcff1zvvfeeHnrooYLu0t/K4sWLNX369ILuxnU5d+6cxo4dqy+//LKgu5InivK6wM1rxowZKlmypHr27FnQXSlQr7zyipYvX54v7do9BuZXHwqDZ555Rh999JF2796d6/de1xnH8ePH67333tPcuXP1r3/9S5I0bNgw1alTR3v27HGq+8ILL+j8+fO5av/48eMaN25crsPZmjVrtGbNmly9J7eu1rd///vfOnToUL7O/0atX79ejRs31osvvqgHH3xQERERBd2lv5WiHFbOnTuncePGFVhwPH/+vF544YU8a68orwvcnNLT0zVjxgwNGDBAxYoVs8qv5zha1OVXaMvNMfDvHBwbNGighg0b6rXXXsv1e68rOLZr104PPvig+vXrp1GjRmn16tVau3atkpKSdM899zht4B4eHvLx8bme2dh27tw5SZKXl5e8vLzydV5X4+npKW9v7wKbvx1JSUkKDAws6G4AWfj4+MjDw6OguwEUmJUrV+qPP/7IcgnRFcfRmwXHwP/To0cPffzxxzpz5kyu3pdn9zjefffdGj16tH755Re9//77Vnl292bExsaqWbNmCgwMVIkSJVStWjU999xzkv66L+iOO+6QJPXr18+6LO6476ZVq1aqXbu2du7cqRYtWsjPz89675X3ODpkZGToueeeU0hIiIoXL6577rlHv/76q1OdnO41u7zNa/Utu3scz549q6eeekphYWHy9vZWtWrV9Oqrr8oY41TPzc1NQ4YM0fLly1W7dm15e3urVq1aWrVqVfYDfoWkpCT1799fwcHB8vHxUb169fTOO+9Y0x33Wx05ckSfffaZ1fer3XuUlpamJ598UrfccotKliype+65R7/99lu2dXft2qV27drJ399fJUqUUOvWrfXNN99kqZeSkqInn3xSlStXlre3typUqKA+ffrozz//lJTzPVGO/l9+NsyxLezZs0ctW7aUn5+fqlatat1T9tVXX6lRo0by9fVVtWrVtHbt2iz9OXbsmB555BEFBwdbY/72229nO+8PP/xQL7/8sipUqCAfHx+1bt1aP/zwg1N/PvvsM/3yyy/W+F7PPa8pKSkaNmyYtc1UrVpVkydPVmZmplXHcT/aq6++qvnz56tKlSry9vbWHXfcoe3bt2dpc+nSpapZs6Z8fHxUu3ZtffLJJ07b688//6xbbrlFkjRu3Dir/1fec3js2DF16dJFJUqU0C233KKnn35aGRkZTnWWLFmiiIgIlSxZUv7+/qpTp45mzJhxzeW+cn6OfccPP/yghx9+WIGBgQoICFC/fv2sL4s5sbMuMjMzr7o+HbZu3aq2bdsqICBAfn5+atmypb7++utrLk920tLS1LFjRwUEBGjLli25Xs5Lly5pwoQJ1vquXLmynnvuOaWlpVl1hg8frtKlSzvtY/71r3/Jzc1NM2fOtMoSExPl5uamuXPnXrXPBw8eVLdu3RQUFCQfHx81bNhQK1asyFJv//79uvvuu+Xr66sKFSropZdectpmHTIzMzV27FiFhobKz89Pd911l7777rts98F2PgvXsmbNGtWvX18+Pj6qWbOmPv74Y6fpycnJevrpp1WnTh2VKFFC/v7+ateuXbaX8GbNmqVatWrJz89PpUqVUsOGDbV48WKnOnb2KTlZvny5KleurCpVqjiVZ3ccvdFjxoULFzR27Fjddttt8vHxUbly5XTffffpxx9/tOrYOX5d7d7Y6/1Mu7m56ezZs3rnnXesz++17gXP62Pgtfpg95h3pZMnT+rOO+9UhQoVrCuUaWlpevHFF1W1alV5e3srLCxMI0eOdPpcO/pkZ52fPn1aw4YNs46zZcuWVZs2bfTtt9861WvTpo3Onj2r2NjYa/b7cnn69f6hhx7Sc889pzVr1ujRRx/Nts7+/fvVsWNH1a1bV+PHj5e3t7d++OEHa0dco0YNjR8/XmPGjNHAgQPVvHlzSVKTJk2sNk6cOKF27dqpZ8+eevDBBxUcHHzVfr388styc3PTM888o6SkJE2fPl1RUVGKj4+Xr6+v7eWz07fLGWN0zz33aMOGDerfv7/q16+v1atXa8SIETp27Jhef/11p/qbN2/Wxx9/rEGDBqlkyZKaOXOmunbtqqNHj6p06dI59uv8+fNq1aqVfvjhBw0ZMkTh4eFaunSpHn74YaWkpGjo0KGqUaOG3nvvPT355JOqUKGCnnrqKUmywkJ2BgwYoPfff1+9evVSkyZNtH79enXo0CFLvf3796t58+by9/fXyJEj5enpqTfffFOtWrWywpv0103JzZs314EDB/TII4/o9ttv159//qkVK1bot99+U5kyZa6+ArJx8uRJdezYUT179lT37t01d+5c9ezZU4sWLdKwYcP02GOPqVevXpo6daq6deumX3/9VSVLlpT014GzcePG1ofxlltu0RdffKH+/fsrNTU1y03TkyZNkru7u55++mmdOnVKU6ZMUe/evbV161ZJ0vPPP69Tp07pt99+s9Ztbn9Qcu7cObVs2VLHjh3TP//5T1WsWFFbtmzRqFGj9Pvvv2e59Lp48WKdPn1a//znP+Xm5qYpU6bovvvu008//SRPT09J0meffab7779fderU0cSJE3Xy5En1799f5cuXt9q55ZZbNHfuXD3++OO69957dd9990mS6tata9XJyMhQTEyMGjVqpFdffVVr167Va6+9pipVqujxxx+X9NeXwgceeECtW7fW5MmTJUkHDhzQ119/raFDh+ZqLBx69Oih8PBwTZw4Ud9++63eeustlS1b1mo/O3bWxbXWp/TXZa127dopIiJCL774otzd3bVgwQLdfffd2rRpk+68807by3H+/Hl17txZO3bs0Nq1a60voblZzgEDBuidd95Rt27d9NRTT2nr1q2aOHGiDhw4oE8++USS1Lx5c73++uvav3+/ateuLUnatGmT3N3dtWnTJj3xxBNWmSS1aNEixz7v379fTZs2Vfny5fXss8+qePHi+vDDD9WlSxd99NFHuvfeeyVJCQkJuuuuu3Tp0iWr3vz587Pdv44aNUpTpkxRp06dFBMTo927dysmJkYXLlxwqpfbz0J2Dh8+rPvvv1+PPfaY+vbtqwULFqh79+5atWqV2rRpI0n66aeftHz5cnXv3l3h4eFKTEzUm2++qZYtW+q7775TaGiopL9uRXriiSfUrVs3DR06VBcuXNCePXu0detW9erVS1Lu9ylX2rJli26//fZrLpfD9R4zMjIy1LFjR61bt049e/bU0KFDdfr0acXGxmrfvn2qUqVKro9fuXGtbf29997TgAEDdOedd2rgwIGSlCVMXy4/joFX64PdY96V/vzzT7Vp00bJycn66quvVKVKFWVmZuqee+7R5s2bNXDgQNWoUUN79+7V66+/ru+//z7LpXI76/yxxx7TsmXLNGTIENWsWVMnTpzQ5s2bdeDAAaftq2bNmvL19dXXX39tfZZtMbmwYMECI8ls3749xzoBAQGmQYMG1usXX3zRXD6b119/3Ugyf/zxR45tbN++3UgyCxYsyDKtZcuWRpKZN29ettNatmxpvd6wYYORZMqXL29SU1Ot8g8//NBIMjNmzLDKKlWqZPr27XvNNq/Wt759+5pKlSpZr5cvX24kmZdeesmpXrdu3Yybm5v54YcfrDJJxsvLy6ls9+7dRpKZNWtWlnldbvr06UaSef/9962yixcvmsjISFOiRAmnZa9UqZLp0KHDVdszxpj4+HgjyQwaNMipvFevXkaSefHFF62yLl26GC8vL/Pjjz9aZcePHzclS5Y0LVq0sMrGjBljJJmPP/44y/wyMzONMf+3jR05csRpumNdbtiwwSpzbAuLFy+2yg4ePGgkGXd3d/PNN99Y5atXr86y3vr372/KlStn/vzzT6d59ezZ0wQEBJhz5845zbtGjRomLS3Nqjdjxgwjyezdu9cq69Chg9M2cC1XbncTJkwwxYsXN99//71TvWeffdYUK1bMHD161BhjzJEjR4wkU7p0aZOcnGzV+/TTT40k87///c8qq1OnjqlQoYI5ffq0Vfbl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acyuNZaOM0UPPvhgjsHg8vv77LSZFzIzM9WmTRuNHDky2+mOg54r+3SteV2ubNmyio+P1+rVq/XFF1/oiy++0IIFC9SnTx+nG9XzYr43uox2t5GpU6fmeJbQzj2snTt31pIlSzRp0iS9++67OX5BtrucdnbyzZo107///W/99NNP2rRpk5o3by43Nzc1a9ZMmzZtUmhoqDIzM62zrNlxLP/TTz+tmJiYbOtcbV9/o3L7Wbher7zyikaPHq1HHnlEEyZMUFBQkNzd3TVs2DCnM8o1atTQoUOHtHLlSq1atUofffSR3njjDY0ZM0bjxo27rn3K5YKCguTm5parL6KF4ZiR2322VDj67Ur33Xef3n33Xc2YMUMTJ050mpaZmak6depo2rRp2b73ypMgdsauR48eat68uT755BOtWbNGU6dO1eTJk/Xxxx+rXbt2Tu87efJklpNr15KnwfG9996TpBx3Mg7u7u5q3bq1WrdurWnTpumVV17R888/rw0bNigqKirPn5DvOHPgYIzRDz/84PQhLlWqlFJSUrK895dffnFK6bnpW6VKlbR27VqdPn3a6VvbwYMHrel5oVKlStqzZ48yMzOdDko3Mp9KlSopMzPT+mbqcOVzKm+55Rb5+fll+/zKgwcPyt3d3drwq1Spon379l11vo5vnleui7z8xijJ+qV4RkaGoqKi8qzdG912q1SpojNnzuRZnxzrPrtfC19ZllefOy8vL3Xq1EmdOnVSZmamBg0apDfffFOjR4/O16BxpbxYF9JflzdvZH106dJF0dHRevjhh1WyZMlr/oo5J47P5OHDh60zKdJfP8hISUlx+pw7AmFsbKy2b9+uZ599VtJfP4SZO3euQkNDVbx48as+w86x3/P09Lzm8leqVCnLflbKur+4fHsMDw+3yk+cOJElMOXFZ8Fx1vTybeH777+XJOtX9suWLdNdd92l//znP07vTUlJyfKDveLFi+v+++/X/fffr4sXL+q+++7Tyy+/rFGjRt3wPsXDw0NVqlTRkSNHcv3e3KpSpYq2bt2q9PR060d0V7J7/MqvfXZuj7V5fQzMqQ+5OeY5/Otf/1LVqlU1ZswYBQQEWJ9H6a91sXv3brVu3TpPs0+5cuU0aNAgDRo0SElJSbr99tv18ssvOwXHS5cu6ddff9U999yTq7bz7B7H9evXa8KECQoPD89yX8vlkpOTs5Q5vs07fnpevHhxSVk3xOv17rvvOt13uWzZMv3+++9OA1ilShV98803unjxolW2cuXKLI/tyU3f2rdvr4yMDM2ePdup/PXXX5ebm1uW5H+92rdvr4SEBH3wwQdW2aVLlzRr1iyVKFFCLVu2zHWbjr5d/vgOSVl+yVisWDFFR0fr008/dbr0n5iYqMWLF6tZs2bWpYeuXbtq9+7d1q8/L+f4tuQ4WF9+/1JGRkaOl36uV7FixdS1a1d99NFH2YbZP/7447raLV68uE6dOnXd/erRo4fi4uK0evXqLNNSUlJ06dKlXLUXGhqq2rVr691333V6VtdXX32lvXv3OtX18/Oz5nO9Tpw44fTa3d3d+oJ25aMl8tuNrouIiAhVqVJFr776arbPOcvNNtKnTx/NnDlT8+bN0zPPPHNd/Wnfvr2krJ9Bx5mKy594EB4ervLly+v1119Xenq6dTm1efPm+vHHH7Vs2TI1btz4qpcqy5Ytq1atWunNN9/U77//nmX65cvfvn17ffPNN9q2bZvT9Mv/bJ4ktW7dWh4eHlnC85X7SClvPgvHjx932t+kpqbq3XffVf369a1LhsWKFctypmvp0qXW/ZsOV27bXl5eqlmzpowxSk9Pz5N9SmRkpEv+PF3Xrl31559/ZjvujrGwe/zy9/dXmTJlstxz+sYbb9xQH4sXL257X5Qfx8Cc+pCbY97lRo8eraefflqjRo1y2v579OihY8eO6d///neW95w/f15nz57NVZ8zMjKy7PfKli2r0NDQLPvg7777ThcuXMjxyTA5ua4zjl988YUOHjyoS5cuKTExUevXr1dsbKwqVaqkFStWXPVBpePHj9fGjRvVoUMHVapUSUlJSXrjjTdUoUIFNWvWTNJf4SEwMFDz5s1TyZIlVbx4cTVq1MjpG2puBAUFqVmzZurXr58SExM1ffp0Va1a1emRQQMGDNCyZcvUtm1b9ejRQz/++KPef//9LPf45aZvnTp10l133aXnn39eP//8s+rVq6c1a9bo008/1bBhw676eIHcGDhwoN588009/PDD2rlzpypXrqxly5bp66+/1vTp07Pco2JH/fr19cADD+iNN97QqVOn1KRJE61bty7bM1cvvfSS9WzOQYMGycPDQ2+++abS0tI0ZcoUq96IESO0bNkyde/eXY888ogiIiKUnJysFStWaN68eapXr55q1aqlxo0ba9SoUUpOTlZQUJCWLFmS68Bkx6RJk7RhwwY1atRIjz76qGrWrKnk5GR9++23Wrt2bbZfcq4lIiJCH3zwgYYPH6477rhDJUqUUKdOnWy/f8SIEVqxYoU6duyohx9+WBERETp79qz27t2rZcuW6eeff871Y4teeeUVde7cWU2bNlW/fv108uRJzZ49W7Vr13YKRL6+vqpZs6Y++OAD3XbbbQoKClLt2rWtR7rYMWDAACUnJ+vuu+9WhQoV9Msvv2jWrFmqX7++01kyV7jRdeHu7q633npL7dq1U61atdSvXz+VL19ex44d04YNG+Tv76///e9/ttsbMmSIUlNT9fzzzysgIMB6/qxd9erVU9++fTV//nylpKSoZcuW2rZtm9555x116dJFd911l1P95s2ba8mSJapTp451Vuj2229X8eLF9f3331/1/kaHOXPmqFmzZqpTp44effRR/eMf/1BiYqLi4uL022+/Wc86HDlypN577z21bdtWQ4cOtR7H4zgT5BAcHKyhQ4fqtdde0z333KO2bdtq9+7d+uKLL1SmTBmnMy558Vm47bbb1L9/f23fvl3BwcF6++23lZiYqAULFlh1OnbsqPHjx6tfv35q0qSJ9u7dq0WLFmW5Hyw6OlohISFq2rSpgoODdeDAAc2ePVsdOnSw9rE3uk/p3Lmz3nvvPX3//fd5dik+O3369NG7776r4cOHa9u2bWrevLnOnj2rtWvXatCgQercuXOujl8DBgzQpEmTNGDAADVs2FAbN260zuxer4iICK1du1bTpk1TaGiowsPDc3zMTX4cA6/WB7vHvCtNnTpVp06d0uDBg1WyZEk9+OCDeuihh/Thhx/qscce04YNG9S0aVNlZGTo4MGD+vDDD7V69Wo1bNjQdp9Pnz6tChUqqFu3bqpXr55KlCihtWvXavv27Vn+SkxsbKz8/PysR1PZlpufYDt+Wu745+XlZUJCQkybNm3MjBkznH7y7nDlYwTWrVtnOnfubEJDQ42Xl5cJDQ01DzzwQJZHLnz66aemZs2axsPDw+mn/i1btszxkQg5PY7nv//9rxk1apQpW7as8fX1NR06dDC//PJLlve/9tprpnz58sbb29s0bdrU7NixI0ubV+vblY/jMcaY06dPmyeffNKEhoYaT09Pc+utt5qpU6c6Pd7DmL9+Zp/dz+dzekzQlRITE02/fv1MmTJljJeXl6lTp062j0fIzaMIzp8/b5544glTunRpU7x4cdOpUyfz66+/ZvvIlm+//dbExMSYEiVKGD8/P3PXXXeZLVu2ZGnzxIkTZsiQIaZ8+fLGy8vLVKhQwfTt29fp8RU//vijiYqKMt7e3iY4ONg899xzJjY2NtvH8WS3LeS0jNmNcWJiohk8eLAJCwsznp6eJiQkxLRu3drMnz/fqnP5Y1Uul91jKM6cOWN69eplAgMDszzuJjvZrd/Tp0+bUaNGmapVqxovLy9TpkwZ06RJE/Pqq6+aixcvOs176tSp2S7nletnyZIlpnr16sbb29vUrl3brFixwnTt2tVUr17dqd6WLVtMRESE8fLycmqnb9++pnjx4lnmdeXne9myZSY6OtqULVvWeHl5mYoVK5p//vOf5vfff7/qOGTXb0fbVz66K6dHNl0pp3WRm/VpjDG7du0y9913nyldurTx9vY2lSpVMj169DDr1q276vxzms/IkSONJDN79uxcL2d6eroZN26cCQ8PN56eniYsLMyMGjXK6XE5DnPmzDGSzOOPP+5UHhUVZSRl6X9Oy//jjz+aPn36mJCQEOPp6WnKly9vOnbsaJYtW+ZUb8+ePaZly5bGx8fHlC9f3kyYMMH85z//ybIMly5dMqNHjzYhISHG19fX3H333ebAgQOmdOnS5rHHHnNq085nISeO/cDq1atN3bp1jbe3t6levXqW9XHhwgXz1FNPmXLlyhlfX1/TtGlTExcXl2Xf/+abb5oWLVpY20GVKlXMiBEjzKlTp5zas7NPyUlaWpopU6aMmTBhglN5To/juZFjxrlz58zzzz9vbUshISGmW7duTo+YsXv8OnfunOnfv78JCAgwJUuWND169LAeC3W9n+mDBw+aFi1aGF9f3yyPaspOfhwDr9YHO8e87B5hmJGRYR544AHj4eFhli9fboz569FBkydPNrVq1TLe3t6mVKlSJiIiwowbN85p+7KzztPS0syIESNMvXr1TMmSJU3x4sVNvXr1rMdzXa5Ro0bmwQcftDUWl3P7/50BcJOpX7++brnlljx9dA5wPVJSUlSqVCm99NJLev755wu6OwVqwoQJWrBggQ4fPuyyH2bi5hMfH6/bb79d3377bY4//stJnj/HEUDhkp6enuVS/5dffqndu3dn+yc6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ffv2mfj4eNO+fXtTsWJFc+bMGavOY489ZsLCwsy6devMjh07TOPGjU2TJk2s6ZcuXTK1a9c2UVFRZteuXebzzz83ZcqUMaNGjSqIRSr0tm3bZipXrmzq1q1rhg4dapUzznkjOTnZVKpUyTz88MNm69at5qeffjKrV682P/zwg1Vn0qRJJiAgwCxfvtzs3r3b3HPPPSY8PNycP3/eqtO2bVtTr149880335hNmzaZqlWrmgceeKAgFqlQevnll03p0qXNypUrzZEjR8zSpUtNiRIlzIwZM6w6jPP1+fzzz83zzz9vPv74YyPJfPLJJ07T82JcT506ZYKDg03v3r3Nvn37zH//+1/j6+tr3nzzTVct5k2F4FhI3HnnnWbw4MHW64yMDBMaGmomTpxYgL0q2pKSkowk89VXXxljjElJSTGenp5m6dKlVp0DBw4YSSYuLs4Y89dOzt3d3SQkJFh15s6da/z9/U1aWpprF6CQO336tLn11ltNbGysadmypRUcGee888wzz5hmzZrlOD0zM9OEhISYqVOnWmUpKSnG29vb/Pe//zXGGPPdd98ZSWb79u1WnS+++MK4ubmZY8eO5V/ni5AOHTqYRx55xKnsvvvuM7179zbGMM555crgmFfj+sYbb5hSpUo57TueeeYZU61atXxeopsTl6oLgYsXL2rnzp2Kioqyytzd3RUVFaW4uLgC7FnRdurUKUlSUFCQJGnnzp1KT093Gufq1aurYsWK1jjHxcWpTp06Tn89KCYmRqmpqdq/f78Le1/4DR48WB06dHAaT4lxzksrVqxQw4YN1b17d5UtW1YNGjTQv//9b2v6kSNHlJCQ4DTWAQEBatSokdNYBwYGqmHDhladqKgoubu7a+vWra5bmEKsSZMmWrdunb7//ntJ0u7du7V582a1a9dOEuOcX/JqXOPi4tSiRQt5eXlZdWJiYnTo0CGdPHnSRUtz87ip/+RgYfHnn38qIyMjy586DA4O1sGDBwuoV0VbZmamhg0bpqZNm6p27dqSpISEBHl5eSkwMNCpbnBwsBISEqw62a0HxzT8ZcmSJfr222+1ffv2LNMY57zz008/ae7cuRo+fLiee+45bd++XU888YS8vLzUt29fa6yyG8vLx7ps2bJO0z08PBQUFMRY/3/PPvusUlNTVb16dRUrVkwZGRl6+eWX1bt3b0linPNJXo1rQkKCwsPDs7ThmFaqVKl86f/NiuCIv6XBgwdr37592rx5c0F35W/n119/1dChQxUbGysfH5+C7s7fWmZmpho2bKhXXnlFktSgQQPt27dP8+bNU9++fQu4d38fH374oRYtWqTFixerVq1aio+P17BhwxQaGso4A1fgUnUhUKZMGRUrVizLr04TExMVEhJSQL0quoYMGaKVK1dqw4YNqlChglUeEhKiixcvKiUlxan+5eMcEhKS7XpwTMNfl6KTkpJ0++23y8PDQx4eHvrqq680c+ZMeXh4KDg4mHHOI+XKlVPNmjWdymrUqKGjR49K+r+xutq+IyQkRElJSU7TL126pOTkZMb6/xsxYoSeffZZ9ezZU3Xq1NFDDz2kJ598UhMnTpTEOOeXvBpX9ieuRXAsBLy8vBQREaF169ZZZZmZmVq3bp0iIyMLsGdFizFGQ4YM0SeffKL169dnuXQREREhT09Pp3E+dOiQjh49ao1zZGSk9u7d67Sjio2Nlb+/f5YD+M2qdevW2rt3r+Lj461/DRs2VO/eva3/Z5zzRtOmTbM8Uur7779XpUqVJEnh4eEKCQlxGuvU1FRt3brVaaxTUlK0c+dOq8769euVmZmpRo0auWApCr9z587J3d35cFisWDFlZmZKYpzzS16Na2RkpDZu3Kj09HSrTmxsrKpVq8Zl6vxQ0L/OwV+WLFlivL29zcKFC813331nBg4caAIDA51+dYqre/zxx01AQID58ssvze+//279O3funFXnscceMxUrVjTr1683O3bsMJGRkSYyMtKa7nhMTHR0tImPjzerVq0yt9xyC4+JuYbLf1VtDOOcV7Zt22Y8PDzMyy+/bA4fPmwWLVpk/Pz8zPvvv2/VmTRpkgkMDDSffvqp2bNnj+ncuXO2jzNp0KCB2bp1q9m8ebO59dZbb/rHxFyub9++pnz58tbjeD7++GNTpkwZM3LkSKsO43x9Tp8+bXbt2mV27dplJJlp06aZXbt2mV9++cUYkzfjmpKSYoKDg81DDz1k9u3bZ5YsWWL8/Px4HE8+ITgWIrNmzTIVK1Y0Xl5e5s477zTffPNNQXepSJGU7b8FCxZYdc6fP28GDRpkSpUqZfz8/My9995rfv/9d6d2fv75Z9OuXTvj6+trypQpY5566imTnp7u4qUpWq4Mjoxz3vnf//5nateubby9vU316tXN/PnznaZnZmaa0aNHm+DgYOPt7W1at25tDh065FTnxIkT5oEHHjAlSpQw/v7+pl+/fub06dOuXIxCLTU11QwdOtRUrFjR+Pj4mH/84x/m+eefd3q8C+N8fTZs2JDtfrlv377GmLwb1927d5tmzZoZb29vU758eTNp0iRXLeJNx82Yyx6NDwAAAOSAexwBAABgC8ERAAAAthAcAQAAYAvBEQAAALYQHAEAAGALwREAAAC2EBwBAABgC8ERAAAAthAcAQAAYAvBEQAAALYQHAEAAGDL/wM6tV3zQ96S4AAAAABJRU5ErkJggg==",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from sentence_transformers import SentenceTransformer\n",
    "\n",
    "# To get the value of the max sequence_length, we will query the underlying `SentenceTransformer` object used in the RecursiveCharacterTextSplitter.\n",
    "print(\n",
    "    f\"Model's maximum sequence length: {SentenceTransformer('thenlper/gte-small').max_seq_length}\"\n",
    ")\n",
    "\n",
    "from transformers import AutoTokenizer\n",
    "\n",
    "tokenizer = AutoTokenizer.from_pretrained(\"thenlper/gte-small\")\n",
    "lengths = [len(tokenizer.encode(doc.page_content)) for doc in tqdm(docs_processed)]\n",
    "\n",
    "# Plot the distribution of document lengths, counted as the number of tokens\n",
    "fig = pd.Series(lengths).hist()\n",
    "plt.title(\"Distribution of document lengths in the knowledge base (in count of tokens)\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "L3teXczl9-9M"
   },
   "source": [
    "👀 可以看到，__片段长度与我们的 512 个 token 的限制不匹配__，并且有些文档超出了限制，因此它们的一部分将在截断中丢失！\n",
    " - 因此，我们应该更改 `RecursiveCharacterTextSplitter` 类，以计算 token 数量而不是字符数量。\n",
    " - 然后，我们可以选择一个特定的片段大小，这里我们会选择低于 512 的阈值：\n",
    "    - 较小的文档可能允许拆分更专注于特定想法的内容。\n",
    "    - 但太小的片段会拆分句子，从而再次失去意义：适当的调整是一个平衡的问题。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "referenced_widgets": [
      "f900cf4ab3a94f45bfa7298f433566ed"
     ]
    },
    "id": "9hvIL2jO9-9M",
    "outputId": "9baf219d-2954-4927-9681-e28572db90db"
   },
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "f900cf4ab3a94f45bfa7298f433566ed",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "  0%|          | 0/17995 [00:00<?, ?it/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAo4AAAGzCAYAAAChApYOAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAABJmElEQVR4nO3de3yP9eP/8edm23u22ea4mTnsQzkfMmGVnGZLS4QQRaI+mDJKpXKuSAepRH0qOvkIlYrEnJORREkUfRTFtqIdnGa21++Pfu/r6+29cW02Gx73282t3tf1er+u1/W6Ts/3dZqHMcYIAAAAOA/Pkm4AAAAALg0ERwAAANhCcAQAAIAtBEcAAADYQnAEAACALQRHAAAA2EJwBAAAgC0ERwAAANhCcAQAAIAtxR4cJ0yYIA8Pj+KejCSpXbt2ateunfV57dq18vDw0KJFiy7K9O+++27VqlXrokyrsI4eParBgwcrNDRUHh4eSkhIKHAdHh4emjBhQpG37UpUq1Yt3X333SXdjPO6++67FRAQUKzTuFjr1cXaL1zs/c+F+vXXX+Xh4aG5c+cWWZ1z586Vh4eHfv311yKr065atWrplltuuejTvVBHjx5VlSpV9P7771vDLuZx9HJXFMdAu5zr/zfffFNs0yisPn36qFevXoX6boGCo7MTnP98fX0VFham2NhYvfTSS8rMzCxUI8528OBBTZgwQdu3by+S+opSaW6bHU8//bTmzp2roUOH6t1339Vdd91V0k26rMybN08vvvhiSTejUI4fP64JEyZo7dq1Jd2UInEpLwtcuWbMmKFy5cqpT58+Jd2UEvX0009r8eLFxVKv3WNgcbWhNHjkkUf04Ycf6rvvvivwdwt1xnHSpEl69913NWvWLN1///2SpISEBDVu3Fjff/+9S9knnnhCJ06cKFD9Bw8e1MSJEwsczlasWKEVK1YU6DsFda62/ec//9FPP/1UrNO/UKtXr1br1q01fvx43XnnnYqMjCzpJl1WLuWwcvz4cU2cOLHEguOJEyf0xBNPFFl9l/KywJUpOztbM2bM0ODBg1WmTBlreGGOo5e64gptBTkGXs7B8ZprrlGLFi30/PPPF/i7hQqOnTt31p133qmBAwdqzJgxWr58uVauXKnU1FTdeuutLiu4l5eXfH19CzMZ244fPy5J8vHxkY+PT7FO61y8vb3lcDhKbPp2pKamKjg4uKSbAbjx9fWVl5dXSTcDKDFLlizRn3/+6XYJ8WIcR68UHAP/T69evfTRRx/p6NGjBfpekd3j2KFDB40dO1a//fab3nvvPWt4XvdmJCYm6oYbblBwcLACAgJUt25dPfbYY5L+uS/o2muvlSQNHDjQuizuvO+mXbt2atSokbZu3aobb7xRfn5+1nfPvsfRKScnR4899phCQ0Pl7++vW2+9VQcOHHApk9+9ZmfWeb625XWP47Fjx/Tggw+qevXqcjgcqlu3rp577jkZY1zKeXh4aPjw4Vq8eLEaNWokh8Ohhg0b6osvvsi7w8+SmpqqQYMGKSQkRL6+vmratKnefvtta7zzfqt9+/Zp6dKlVtvPde9RVlaWRo4cqcqVK6tcuXK69dZb9fvvv+dZdtu2bercubMCAwMVEBCgjh07atOmTW7l0tLSNHLkSNWqVUsOh0Ph4eHq37+//vrrL0n53xPlbP+ZZ8Oc68L333+vtm3bys/PT3Xq1LHuKVu3bp1atWqlsmXLqm7dulq5cqVbe/744w/dc889CgkJsfr8rbfeynPaCxYs0FNPPaXw8HD5+vqqY8eO2rt3r0t7li5dqt9++83q38Lc85qWlqaEhARrnalTp46eeeYZ5ebmWmWc96M999xzev3111W7dm05HA5de+212rJli1udCxcuVIMGDeTr66tGjRrp448/dllff/31V1WuXFmSNHHiRKv9Z99z+Mcff6hbt24KCAhQ5cqV9dBDDyknJ8elzPz58xUZGaly5copMDBQjRs31owZM84732dPz7nv2Lt3r+6++24FBwcrKChIAwcOtH4s5sfOssjNzT3n8nTavHmzbrrpJgUFBcnPz09t27bVV199dd75yUtWVpZuueUWBQUFaePGjQWez9OnT2vy5MnW8q5Vq5Yee+wxZWVlWWVGjRqlihUruuxj7r//fnl4eOill16yhqWkpMjDw0OzZs06Z5t3796tnj17qkKFCvL19VWLFi306aefupXbuXOnOnTooLJlyyo8PFxPPvmkyzrrlJubqwkTJigsLEx+fn5q3769fvzxxzz3wXa2hfNZsWKFmjVrJl9fXzVo0EAfffSRy/gjR47ooYceUuPGjRUQEKDAwEB17tw5z0t4L7/8sho2bCg/Pz+VL19eLVq00Lx581zK2Nmn5Gfx4sWqVauWateu7TI8r+PohR4zTp48qQkTJujqq6+Wr6+vqlatqu7du+uXX36xytg5fp3r3tjCbtMeHh46duyY3n77bWv7Pd+94EV9DDxfG+we8872999/q2XLlgoPD7euUGZlZWn8+PGqU6eOHA6Hqlevrocffthlu3a2yc4yz8zMVEJCgnWcrVKlijp16qRvv/3WpVynTp107NgxJSYmnrfdZyrSn/d33XWXHnvsMa1YsUL33ntvnmV27typW265RU2aNNGkSZPkcDi0d+9ea0dcv359TZo0SePGjdN9992nNm3aSJKuu+46q47Dhw+rc+fO6tOnj+68806FhIScs11PPfWUPDw89Mgjjyg1NVUvvviioqOjtX37dpUtW9b2/Nlp25mMMbr11lu1Zs0aDRo0SM2aNdPy5cs1evRo/fHHH5o+fbpL+Q0bNuijjz7SsGHDVK5cOb300kvq0aOH9u/fr4oVK+bbrhMnTqhdu3bau3evhg8froiICC1cuFB333230tLSNGLECNWvX1/vvvuuRo4cqfDwcD344IOSZIWFvAwePFjvvfee+vbtq+uuu06rV69WXFycW7mdO3eqTZs2CgwM1MMPPyxvb2+99tprateunRXepH9uSm7Tpo127dqle+65R82bN9dff/2lTz/9VL///rsqVap07gWQh7///lu33HKL+vTpo9tvv12zZs1Snz599P777yshIUFDhgxR37599eyzz6pnz546cOCAypUrJ+mfA2fr1q2tjbFy5cpatmyZBg0apIyMDLebpqdOnSpPT0899NBDSk9P17Rp09SvXz9t3rxZkvT4448rPT1dv//+u7VsC/pAyfHjx9W2bVv98ccf+ve//60aNWpo48aNGjNmjA4dOuR26XXevHnKzMzUv//9b3l4eGjatGnq3r27/ve//8nb21uStHTpUvXu3VuNGzfWlClT9Pfff2vQoEGqVq2aVU/lypU1a9YsDR06VLfddpu6d+8uSWrSpIlVJicnR7GxsWrVqpWee+45rVy5Us8//7xq166toUOHSvrnR+Edd9yhjh076plnnpEk7dq1S1999ZVGjBhRoL5w6tWrlyIiIjRlyhR9++23euONN1SlShWr/rzYWRbnW57SP5e1OnfurMjISI0fP16enp6aM2eOOnTooC+//FItW7a0PR8nTpxQ165d9c0332jlypXWj9CCzOfgwYP19ttvq2fPnnrwwQe1efNmTZkyRbt27dLHH38sSWrTpo2mT5+unTt3qlGjRpKkL7/8Up6envryyy/1wAMPWMMk6cYbb8y3zTt37tT111+vatWq6dFHH5W/v78WLFigbt266cMPP9Rtt90mSUpOTlb79u11+vRpq9zrr7+e5/51zJgxmjZtmrp06aLY2Fh99913io2N1cmTJ13KFXRbyMuePXvUu3dvDRkyRAMGDNCcOXN0++2364svvlCnTp0kSf/73/+0ePFi3X777YqIiFBKSopee+01tW3bVj/++KPCwsIk/XMr0gMPPKCePXtqxIgROnnypL7//ntt3rxZffv2lVTwfcrZNm7cqObNm593vpwKe8zIycnRLbfcolWrVqlPnz4aMWKEMjMzlZiYqB9++EG1a9cu8PGrIM63rr/77rsaPHiwWrZsqfvuu0+S3ML0mYrjGHiuNtg95p3tr7/+UqdOnXTkyBGtW7dOtWvXVm5urm699VZt2LBB9913n+rXr68dO3Zo+vTp+vnnn90uldtZ5kOGDNGiRYs0fPhwNWjQQIcPH9aGDRu0a9cul/WrQYMGKlu2rL766itrW7bFFMCcOXOMJLNly5Z8ywQFBZlrrrnG+jx+/Hhz5mSmT59uJJk///wz3zq2bNliJJk5c+a4jWvbtq2RZGbPnp3nuLZt21qf16xZYySZatWqmYyMDGv4ggULjCQzY8YMa1jNmjXNgAEDzlvnudo2YMAAU7NmTevz4sWLjSTz5JNPupTr2bOn8fDwMHv37rWGSTI+Pj4uw7777jsjybz88stu0zrTiy++aCSZ9957zxp26tQpExUVZQICAlzmvWbNmiYuLu6c9RljzPbt240kM2zYMJfhffv2NZLM+PHjrWHdunUzPj4+5pdffrGGHTx40JQrV87ceOON1rBx48YZSeajjz5ym15ubq4x5v/WsX379rmMdy7LNWvWWMOc68K8efOsYbt37zaSjKenp9m0aZM1fPny5W7LbdCgQaZq1armr7/+cplWnz59TFBQkDl+/LjLtOvXr2+ysrKscjNmzDCSzI4dO6xhcXFxLuvA+Zy93k2ePNn4+/ubn3/+2aXco48+asqUKWP2799vjDFm3759RpKpWLGiOXLkiFXuk08+MZLMZ599Zg1r3LixCQ8PN5mZmdawtWvXGkkubf3zzz/d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z06dRSE3N1edOnXSww8/nOd450HvYrbpfNM6U5UqVbR9+3YtX75cy5Yt07JlyzRnzhz179/f5Ub1opjuhc6j3XXk2WefzfcsoZ17WLt27ar58+dr6tSpeuedd/L9gWx3Pu3s5G+44Qb95z//0f/+9z99+eWXatOmjTw8PHTDDTfoyy+/VFhYmHJzc62zrHlxzv9DDz2k2NjYPMuca19/oQq6LRTW008/rbFjx+qee+7R5MmTVaFCBXl6eiohIcHljHL9+vX1008/acmSJfriiy/04Ycf6tVXX9W4ceM0ceLEQu1TzlShQgV5eHgU6IdoaThmFHSfLZWOdl9M3bt31zvvvKMZM2ZoypQpLuNyc3PVuHFjvfDCC3l+9+yTIHb6rlevXmrTpo0+/vhjrVixQs8++6yeeeYZffTRR+rcubPL9/7++2+3k2vnU6TB8d1335WkfHcyTp6enurYsaM6duyoF154QU8//bQef/xxrVmzRtHR0UX+hnznmQMnY4z27t3rshGXL19eaWlpbt/97bffXFJ6QdpWs2ZNrVy5UpmZmS6/2nbv3m2NLwo1a9bU999/r9zcXJeD0oVMp2bNmsrNzbV+mTqd/Z7KypUry8/PL8/3V+7evVuenp7Wil+7dm398MMP55yu85fn2cuiKH8xSrKeFM/JyVF0dHSR1Xuh627t2rV19OjRImuTc9nn9bTw2cOKarvz8fFRly5d1KVLF+Xm5mrYsGF67bXXNHbs2GINGmcrimUh/XN580KWR7du3RQTE6O7775b5cqVO+9TzPlxbpN79uyxzqRI/zyQkZaW5rKdOwNhYmKitmzZokcffVTSPw/CzJo1S2FhYfL39z/nO+yc+z1vb+/zzn/NmjXd9rOS+/7izPUxIiLCGn748GG3wFQU24LzrOmZ68LPP/8sSdZT9osWLVL79u315ptvunw3LS3N7YE9f39/9e7dW71799apU6fUvXt3PfXUUxozZswF71O8vLxUu3Zt7du3r8DfLajatWtr8+bNys7Oth6iO5vd41dx7bMLeqwt6mNgfm0oyDHP6f7771edOnU0btw4BQUFWduj9M+y+O6779SxY8cizT5Vq1bVsGHDNGzYMKWmpqp58+Z66qmnXILj6dOndeDAAd16660FqrvI7nFcvXq1Jk+erIiICLf7Ws505MgRt2HOX/POR8/9/f0lua+IhfXOO++43He5aNEiHTp0yKUDa9eurU2bNunUqVPWsCVLlri9tqcgbbv55puVk5OjV155xWX49OnT5eHh4Zb8C+vmm29WcnKyPvjgA2vY6dOn9fLLLysgIEBt27YtcJ3Otp35+g5Jbk8ylilTRjExMfrkk09cLv2npKRo3rx5uuGGG6xLDz169NB3331nPf15JuevJefB+sz7l3JycvK99FNYZcqUUY8ePfThhx/mGWb//PPPQtXr7++v9PT0QrerV69eSkpK0vLly93GpaWl6fTp0wWqLywsTI0aNdI777zj8q6udevWaceOHS5l/fz8rOkU1uHDh10+e3p6Wj/Qzn61RHG70GURGRmp2rVr67nnnsvzPWcFWUf69++vl156SbNnz9YjjzxSqPbcfPPNkty3QeeZijPfeBAREaFq1app+vTpys7Oti6ntmnTRr/88osWLVqk1q1bn/NSZZUqVdSuXTu99tprOnTokNv4M+f/5ptv1qZNm/T111+7jD/zz+ZJUseOHeXl5eUWns/eR0pFsy0cPHjQZX+TkZGhd955R82aNbMuGZYpU8btTNfChQut+zedzl63fXx81KBBAxljlJ2dXST7lKioqIvy5+l69Oihv/76K89+d/aF3eNXYGCgKlWq5HbP6auvvnpBbfT397e9LyqOY2B+bSjIMe9MY8eO1UMPPaQxY8a4rP+9evXSH3/8of/85z9u3zlx4oSOHTtWoDbn5OS47feqVKmisLAwt33wjz/+qJMnT+b7Zpj8FOqM47Jly7R7926dPn1aKSkpWr16tRITE1WzZk19+umn53xR6aRJk7R+/XrFxcWpZs2aSk1N1auvvqrw8HDdcMMNkv4JD8HBwZo9e7bKlSsnf39/tWrVyuUXakFUqFBBN9xwgwYOHKiUlBS9+OKLqlOnjssrgwYPHqxFixbppptuUq9evfTLL7/ovffec7vHryBt69Kli9q3b6/HH39cv/76q5o2baoVK1bok08+UUJCwjlfL1AQ9913n1577TXdfffd2rp1q2rVqqVFixbpq6++0osvvuh2j4odzZo10x133KFXX31V6enpuu6667Rq1ao8z1w9+eST1rs5hw0bJi8vL7322mvKysrStGnTrHKjR4/WokWLdPvtt+uee+5RZGSkjhw5ok8//VSzZ89W06ZN1bBhQ7Vu3VpjxozRkSNHVKFCBc2fP7/AgcmOqVOnas2aNWrVqpXuvfdeNWjQQEeOHNG3336rlStX5vkj53wiIyP1wQcfaNSoUbr22msVEBCgLl262P7+6NGj9emnn+qWW27R3XffrcjISB07dkw7duzQokWL9Ouvvxb4tUVPP/20unbtquuvv14DBw7U33//rVdeeUWNGjVyCURly5ZVgwYN9MEHH+jqq69WhQoV1KhRI+uVLnYMHjxYR44cUYcOHRQeHq7ffvtNL7/8spo1a+ZyluxiuNBl4enpqTfeeEOdO3dWw4YNNXDgQFWrVk1//PGH1qxZo8DAQH322We26xs+fLgyMjL0+OOPKygoyHr/rF1NmzbVgAED9PrrrystLU1t27bV119/rbffflvdunVT+/btXcq3adNG8+fPV+PGja2zQs2bN5e/v79+/vnnc97f6DRz5kzdcMMNaty4se69917961//UkpKipKSkvT7779b7zp8+OGH9e677+qmm27SiBEjrNfxOM8EOYWEhGjEiBF6/vnndeutt+qmm27Sd999p2XLlqlSpUouZ1yKYlu4+uqrNWjQIG3ZskUhISF66623lJKSojlz5lhlbrnlFk2aNEkDBw7Uddddpx07duj99993ux8sJiZGoaGhuv766xUSEqJdu3bplVdeUVxcnLWPvdB9SteuXfXuu+/q559/LrJL8Xnp37+/3nnnHY0aNUpff/212rRpo2PHjmnlypUaNmyYunbtWqDj1+DBgzV16lQNHjxYLVq00Pr1660zu4UVGRmplStX6oUXXlBYWJgiIiLyfc1NcRwDz9UGu8e8sz377LNKT09XfHy8ypUrpzvvvFN33XWXFixYoCFDhmjNmjW6/vrrlZOTo927d2vBggVavny5WrRoYbvNmZmZCg8PV8+ePdW0aVMFBARo5cqV2rJli9tfiUlMTJSfn5/1airbCvIItvPRcuc/Hx8fExoaajp16mRmzJjh8si709mvEVi1apXp2rWrCQsLMz4+PiYsLMzccccdbq9c+OSTT0yDBg2Ml5eXy6P+bdu2zfeVCPm9jue///2vGTNmjKlSpYopW7asiYuLM7/99pvb959//nlTrVo143A4zPXXX2+++eYbtzrP1bazX8djjDGZmZlm5MiRJiwszHh7e5urrrrKPPvssy6v9zDmn8fs83p8Pr/XBJ0tJSXFDBw40FSqVMn4+PiYxo0b5/l6hIK8iuDEiRPmgQceMBUrVjT+/v6mS5cu5sCBA3m+suXbb781sbGxJiAgwPj5+Zn27dubjRs3utV5+PBhM3z4cFOtWjXj4+NjwsPDzYABA1xeX/HLL7+Y6Oho43A4TEhIiHnsscdMYmJinq/jyWtdyG8e8+rjlJQUEx8fb6pXr268vb1NaGio6dixo3n99detMme+VuVMeb2G4ujRo6Zv374mODjY7XU3eclr+WZmZpoxY8aYOnXqGB8fH1OpUiVz3XXXmeeee86cOnXKZdrPPvtsnvN59vKZP3++qVevnnE4HKZRo0bm008/NT169DD16tVzKbdx40YTGRlpfHx8XOoZMGCA8ff3d5vW2dv3okWLTExMjKlSpYrx8fExNWrUMP/+97/NoUOHztkPebXbWffZr+7K75VNZ8tvWRRkeRpjzLZt20z37t1NxYoVjcPhMDVr1jS9evUyq1atOuf085vOww8/bCSZV155pcDzmZ2dbSZOnGgiIiKMt7e3qV69uhkzZozL63KcZs6caSSZoUOHugyPjo42ktzan9/8//LLL6Z///4mNDTUeHt7m2rVqplbbrnFLFq0yKXc999/b9q2bWt8fX1NtWrVzOTJk82bb77pNg+nT582Y8eONaGhoaZs2bKmQ4cOZteuXaZixYpmyJAhLnXa2Rby49wPLF++3DRp0sQ4HA5Tr149t+Vx8uRJ8+CDD5qqVauasmXLmuuvv94kJSW57ftfe+01c+ONN1rrQe3atc3o0aNNenq6S3129in5ycrKMpUqVTKTJ092GZ7f63gu5Jhx/Phx8/jjj1vrUmhoqOnZs6fLK2bsHr+OHz9uBg0aZIKCgky5cuVMr169rNdCFXab3r17t7nxxhtN2bJl3V7VlJfiOAaeqw12jnl5vcIwJyfH3HHHHcbLy8ssXrzYGPPPq4OeeeYZ07BhQ+NwOEz58uVNZGSkmThxosv6ZWeZZ2VlmdGjR5umTZuacuXKGX9/f9O0aVPr9VxnatWqlbnzzjtt9cWZPP5/YwBcYZo1a6bKlSsX6atzgMJIS0tT+fLl9eSTT+rxxx8v6eaUqMmTJ2vOnDnas2fPRXswE1ee7du3q3nz5vr222/zffgvP0X+HkcApUt2drbbpf61a9fqu+++y/NPdALF6cSJE27DnPdtsj5KI0eO1NGjRzV//vySbgouY1OnTlXPnj0LHBoliTOOwGXu119/VXR0tO68806FhYVp9+7dmj17toKCgvTDDz+c80+TAUVt7ty5mjt3rm6++WYFBARow4YN+u9//6uYmJg8H4QBULoU+QvAAZQu5cuXV2RkpN544w39+eef8vf3V1xcnKZOnUpoxEXXpEkTeXl5adq0acrIyLAemHnyySdLumkAbOCMIwAAAGzhHkcAAADYQnAEAACALdzjWEi5ubk6ePCgypUrV+R/IhEAABQPY4wyMzMVFhaW79+OR/4IjoV08OBBt79HCQAALg0HDhxQeHh4STfjkkNwLCTnnzA6cOBAnn+XsqCys7O1YsUKxcTE5PtH53Fh6OPiRx8XL/q3+NHHxa+k+zgjI0PVq1cv9J8ivNIRHAvJeXk6MDCwyIKjn5+fAgMD2VkVE/q4+NHHxYv+LX70cfErLX3MbWaFw8V9AAAA2EJwBAAAgC0ERwAAANhCcAQAAIAtBEcAAADYQnAEAACALQRHAAAA2EJwBAAAgC0ERwAAANhCcAQAAIAtBEcAAADYQnAEAACALQRHAAAA2EJwBAAAgC1eJd0AAAAuJbUeXVrSTSiwX6fGlXQTcJngjCMAAABsITgCAADAFoIjAAAAbCE4AgAAwBaCIwAAAGwhOAIAAMAWgiMAAABsITgCAADAFoIjAAAAbCE4AgAAwBaCIwAAAGwhOAIAAMAWgiMAAABsITgCAADAFoIjAAAAbCE4AgAAwBaCIwAAAGwhOAIAAMAWgiMAAABsITgCAADAFoIjAAAAbCE4AgAAwBaCIwAAAGwhOAIAAMAWgiMAAABsITgCAADAFoIjAAAAbCE4AgAAwBaCIwAAAGwhOAIAAMCWUh0cp06dKg8PDyUkJFjDTp48qfj4eFWsWFEBAQHq0aOHUlJSXL63f/9+xcXFyc/PT1WqVNHo0aN1+vRplzJr165V8+bN5XA4VKdOHc2dO/cizBEAAMClq9QGxy1btui1115TkyZNXIaPHDlSn332mRYuXKh169bp4MGD6t69uzU+JydHcXFxOnXqlDZu3Ki3335bc+fO1bhx46wy+/btU1xcnNq3b6/t27crISFBgwcP1vLlyy/a/AEAAFxqSmVwPHr0qPr166f//Oc/Kl++vDU8PT1db775pl544QV16NBBkZGRmjNnjjZu3KhNmzZJklasWKEff/xR7733npo1a6bOnTtr8uTJmjlzpk6dOiVJmj17tiIiIvT888+rfv36Gj58uHr27Knp06eXyPwCAABcCrxKugF5iY+PV1xcnKKjo/Xkk09aw7du3ars7GxFR0dbw+rVq6caNWooKSlJrVu3VlJSkho3bqyQkBCrTGxsrIYOHaqdO3fqmmuuUVJSkksdzjJnXhI/W1ZWlrKysqzPGRkZkqTs7GxlZ2df6CxbdRRFXcgbfVz86OPiRf8WPzt97ChjLlZzikxpWmdKej0uTX1xKSp1wXH+/Pn69ttvtWXLFrdxycnJ8vHxUXBwsMvwkJAQJScnW2XODI3O8c5x5yqTkZGhEydOqGzZsm7TnjJliiZOnOg2fMWKFfLz87M/g+eRmJhYZHUhb/Rx8aOPixf9W/zO1cfTWl7EhhSRzz//vKSb4Kak1uPjx4+XyHQvF6UqOB44cEAjRoxQYmKifH19S7o5LsaMGaNRo0ZZnzMyMlS9enXFxMQoMDDwguvPzs5WYmKiOnXqJG9v7wuuD+7o4+JHHxcv+rf42enjRhMuvfvhf5gQW9JNsJT0euy8YojCKVXBcevWrUpNTVXz5s2tYTk5OVq/fr1eeeUVLV++XKdOnVJaWprLWceUlBSFhoZKkkJDQ/X111+71Ot86vrMMmc/iZ2SkqLAwMA8zzZKksPhkMPhcBvu7e1dpCt+UdcHd/Rx8aOPixf9W/zO1cdZOR4XuTUXrjSuLyW1HpfGvriUlKqHYzp27KgdO3Zo+/bt1r8WLVqoX79+1v97e3tr1apV1nd++ukn7d+/X1FRUZKkqKgo7dixQ6mpqVaZxMREBQYGqkGDBlaZM+twlnHWAQAAAHel6oxjuXLl1KhRI5dh/v7+qlixojV80KBBGjVqlCpUqKDAwEDdf//9ioqKUuvWrSVJMTExatCgge666y5NmzZNycnJeuKJJxQfH2+dMRwyZIheeeUVPfzww7rnnnu0evVqLViwQEuXLr24MwwAAHAJKVXB0Y7p06fL09NTPXr0UFZWlmJjY/Xqq69a48uUKaMlS5Zo6NChioqKkr+/vwYMGKBJkyZZZSIiIrR06VKNHDlSM2bMUHh4uN544w3Fxpaee0AAAABKm1IfHNeuXevy2dfXVzNnztTMmTPz/U7NmjXP+wRZu3bttG3btqJoIgAAwBWhVN3jCAAAgNKL4AgAAABbCI4AAACwheAIAAAAWwiOAAAAsIXgCAAAAFsIjgAAALCF4AgAAABbCI4AAACwheAIAAAAWwiOAAAAsIXgCAAAAFsIjgAAALCF4AgAAABbCI4AAACwheAIAAAAWwiOAAAAsIXgCAAAAFsIjgAAALCF4AgAAABbCI4AAACwheAIAAAAWwiOAAAAsIXgCAAAAFsIjgAAALCF4AgAAABbCI4AAACwheAIAAAAWwiOAAAAsIXgCAAAAFsIjgAAALCF4AgAAABbCI4AAACwheAIAAAAWwiOAAAAsIXgCAAAAFsIjgAAALCF4AgAAABbCI4AAACwheAIAAAAWwiOAAAAsIXgCAAAAFsIjgAAALCF4AgAAABbCI4AAACwheAIAAAAWwiOAAAAsIXgCAAAAFsIjgAAALCF4AgAAABbCI4AAACwheAIAAAAWwiOAAAAsMWrpBsAAACKV61Hl5Z0EyyOMkbTWkqNJixXVo7HOcv+OjXuIrUKdnHGEQAAALYQHAEAAGALwREAAAC2EBwBAABgC8ERAAAAthAcAQAAYAvBEQAAALYQHAEAAGALwREAAAC2EBwBAABgC8ERAAAAthAcAQAAYAvBEQAAALYQHAEAAGBLqQqOs2bNUpMmTRQYGKjAwEBFRUVp2bJl1viTJ08qPj5eFStWVEBAgHr06KGUlBSXOvbv36+4uDj5+fmpSpUqGj16tE6fPu1SZu3atWrevLkcDofq1KmjuXPnXozZAwAAuKSVquAYHh6uqVOnauvWrfrmm2/UoUMHde3aVTt37pQkjRw5Up999pkWLlyodevW6eDBg+revbv1/ZycHMXFxenUqVPauHGj3n77bc2dO1fjxo2zyuzbt09xcXFq3769tm/froSEBA0ePFjLly+/6PMLAABwKfEq6QacqUuXLi6fn3rqKc2aNUubNm1SeHi43nzzTc2bN08dOnSQJM2ZM0f169fXpk2b1Lp1a61YsUI//vijVq5cqZCQEDVr1kyTJ0/WI488ogkTJsjHx0ezZ89WRESEnn/+eUlS/fr1tWHDBk2fPl2xsbEXfZ4BAAAuFaUqOJ4pJydHCxcu1LFjxxQVFaWtW7cqOztb0dHRVpl69eqpRo0aSkpKUuvWrZWUlKTGjRsrJCTEKhMbG6uhQ4dq586duuaaa5SUlORSh7NMQkLCOduTlZWlrKws63NGRoYkKTs7W9nZ2Rc8v846iqIu5I0+Ln70cfGif4ufnT52lDEXqzmXJYencfnvuRTHus72c2FKXXDcsWOHoqKidPLkSQUEBOjjjz9WgwYNtH37dvn4+Cg4ONilfEhIiJKTkyVJycnJLqHROd457lxlMjIydOLECZUtWzbPdk2ZMkUTJ050G75ixQr5+fkVal7zkpiYWGR1IW/0cfGjj4sX/Vv8ztXH01pexIZcxia3yD1vmc8//7zIp3v8+PEir/NKUuqCY926dbV9+3alp6dr0aJFGjBggNatW1fSzdKYMWM0atQo63NGRoaqV6+umJgYBQYGXnD92dnZSkxMVKdOneTt7X3B9cEdfVz86OPiRf8WPzt93GgC98RfCIen0eQWuRr7jaeycj3OWfaHCUV/C5nziiEKp9QFRx8fH9WpU0eSFBkZqS1btmjGjBnq3bu3Tp06pbS0NJezjikpKQoNDZUkhYaG6uuvv3apz/nU9Zllzn4SOyUlRYGBgfmebZQkh8Mhh8PhNtzb27tId+BFXR/c0cfFjz4uXvRv8TtXH2flnDvswJ6sXI/z9mVxrOdsOxemVD1VnZfc3FxlZWUpMjJS3t7eWrVqlTXup59+0v79+xUVFSVJioqK0o4dO5SammqVSUxMVGBgoBo0aGCVObMOZxlnHQAAAMhbqTrjOGbMGHXu3Fk1atRQZmam5s2bp7Vr12r58uUKCgrSoEGDNGrUKFWoUEGBgYG6//77FRUVpdatW0uSYmJi1KBBA911112aNm2akpOT9cQTTyg+Pt46WzhkyBC98sorevjhh3XPPfdo9erVWrBggZYuXVqSsw4AAFDqlargmJqaqv79++vQoUMKCgpSkyZNtHz5cnXq1EmSNH36dHl6eqpHjx7KyspSbGysXn31Vev7ZcqU0ZIlSzR06FBFRUXJ399fAwYM0KRJk6wyERERWrp0qUaOHKkZM2YoPDxcb7zxBq/iAQAAOI9SFRzffPPNc4739fXVzJkzNXPmzHzL1KxZ87xPYbVr107btm0rVBsBAACuVKX+HkcAAACUDgRHAAAA2EJwBAAAgC0ERwAAANhCcAQAAIAtBEcAAADYQnAEAACALQRHAAAA2EJwBAAAgC0ERwAAANhCcAQAAIAtBEcAAADYQnAEAACALQRHAAAA2EJwBAAAgC0ERwAAANhCcAQAAIAtBEcAAADYQnAEAACALQRHAAAA2EJwBAAAgC0ERwAAANhCcAQAAIAtBEcAAADYQnAEAACALQRHAAAA2EJwBAAAgC1eJd0AAMCVq9ajS0u6CS4cZYymtZQaTViurByPkm4OUOpwxhEAAAC2EBwBAABgC8ERAAAAthAcAQAAYAvBEQAAALYQHAEAAGALwREAAAC2EBwBAABgC8ERAAAAthAcAQAAYAvBEQAAALYQHAEAAGALwREAAAC2EBwBAABgC8ERAAAAthAcAQAAYAvBEQAAALYQHAEAAGALwREAAAC2EBwBAABgC8ERAAAAthAcAQAAYAvBEQAAALYQHAEAAGALwREAAAC2EBwBAABgC8ERAAAAthAcAQAAYAvBEQAAALYQHAEAAGALwREAAAC2EBwBAABgC8ERAAAAthAcAQAAYAvBEQAAALYQHAEAAGALwREAAAC2EBwBAABgC8ERAAAAthAcAQAAYAvBEQAAALaUquA4ZcoUXXvttSpXrpyqVKmibt266aeffnIpc/LkScXHx6tixYoKCAhQjx49lJKS4lJm//79iouLk5+fn6pUqaLRo0fr9OnTLmXWrl2r5s2by+FwqE6dOpo7d25xzx4AAMAlrVQFx3Xr1ik+Pl6bNm1SYmKisrOzFRMTo2PHjlllRo4cqc8++0wLFy7UunXrdPDgQXXv3t0an5OTo7i4OJ06dUobN27U22+/rblz52rcuHFWmX379ikuLk7t27fX9u3blZCQoMGDB2v58uUXdX4BAAAuJV4l3YAzffHFFy6f586dqypVqmjr1q268cYblZ6erjfffFPz5s1Thw4dJElz5sxR/fr1tWnTJrVu3VorVqzQjz/+qJUrVyokJETNmjXT5MmT9cgjj2jChAny8fHR7NmzFRERoeeff16SVL9+fW3YsEHTp09XbGxsnm3LyspSVlaW9TkjI0OSlJ2drezs7Aued2cdRVEX8kYfFz/6uHhdjv3rKGNKugkuHJ7G5b8oegXp4+JY1y+n7acklKrgeLb09HRJUoUKFSRJW7duVXZ2tqKjo60y9erVU40aNZSUlKTWrVsrKSlJjRs3VkhIiFUmNjZWQ4cO1c6dO3XNNdcoKSnJpQ5nmYSEhHzbMmXKFE2cONFt+IoVK+Tn53chs+kiMTGxyOpC3ujj4kcfF6/LqX+ntSzpFuRtcovckm7CZc9OH3/++edFPt3jx48XeZ1XklIbHHNzc5WQkKDrr79ejRo1kiQlJyfLx8dHwcHBLmVDQkKUnJxslTkzNDrHO8edq0xGRoZOnDihsmXLurVnzJgxGjVqlPU5IyND1atXV0xMjAIDAy9sZvXPL6DExER16tRJ3t7eF1wf3NHHxY8+Ll6XY/82mlC6bhFyeBpNbpGrsd94KivXo6Sbc1kqSB//MCHvq4AXwnnFEIVTaoNjfHy8fvjhB23YsKGkmyJJcjgccjgcbsO9vb2LdAde1PXBHX1c/Ojj4nU59W9WTukMZ1m5HqW2bZcLO31cHOv55bLtlJRS9XCM0/Dhw7VkyRKtWbNG4eHh1vDQ0FCdOnVKaWlpLuVTUlIUGhpqlTn7KWvn5/OVCQwMzPNsIwAAAEpZcDTGaPjw4fr444+1evVqRUREuIyPjIyUt7e3Vq1aZQ376aeftH//fkVFRUmSoqKitGPHDqWmplplEhMTFRgYqAYNGlhlzqzDWcZZBwAAANyVqkvV8fHxmjdvnj755BOVK1fOuicxKChIZcuWVVBQkAYNGqRRo0apQoUKCgwM1P3336+oqCi1bt1akhQTE6MGDRrorrvu0rRp05ScnKwnnnhC8fHx1qXmIUOG6JVXXtHDDz+se+65R6tXr9aCBQu0dOnSEpt3AACA0q5UnXGcNWuW0tPT1a5dO1WtWtX698EHH1hlpk+frltuuUU9evTQjTfeqNDQUH300UfW+DJlymjJkiUqU6aMoqKidOedd6p///6aNGmSVSYiIkJLly5VYmKimjZtqueff15vvPFGvq/iAQAAQCk742jM+d/p5Ovrq5kzZ2rmzJn5lqlZs+Z5H+Fv166dtm3bVuA2AgAAXKlKVXAEABRerUe53QZA8SpVl6oBAABQehEcAQAAYAvBEQAAALYQHAEAAGALwREAAAC2EBwBAABgC8ERAAAAthAcAQAAYAvBEQAAALYQHAEAAGALwREAAAC2EBwBAABgC8ERAAAAthAcAQAAYAvBEQAAALYQHAEAAGALwREAAAC2EBwBAABgC8ERAAAAthAcAQAAYAvBEQAAALYQHAEAAGALwREAAAC2EBwBAABgC8ERAAAAthAcAQAAYAvBEQAAALYQHAEAAGALwREAAAC2EBwBAABgC8ERAAAAthAcAQAAYAvBEQAAALYQHAEAAGALwREAAAC2EBwBAABgC8ERAAAAthAcAQAAYAvBEQAAALYQHAEAAGALwREAAAC2EBwBAABgi1dJNwAASqNajy4t6Sa4cZQxmtZSajRhubJyPEq6OQCuQJxxBAAAgC0ERwAAANhCcAQAAIAtBEcAAADYQnAEAACALQRHAAAA2EJwBAAAgC28xxFXtNL4rr7z+XVqXEk3AQBwheKMIwAAAGwhOAIAAMAWgiMAAABsITgCAADAFoIjAAAAbCE4AgAAwBaCIwAAAGwhOAIAAMAWgiMAAABsITgCAADAFoIjAAAAbCE4AgAAwBaCIwAAAGwhOAIAAMAWgiMAAABsITgCAADAllIXHNevX68uXbooLCxMHh4eWrx4sct4Y4zGjRunqlWrqmzZsoqOjtaePXtcyhw5ckT9+vVTYGCggoODNWjQIB09etSlzPfff682bdrI19dX1atX17Rp04p71gAAAC5ppS44Hjt2TE2bNtXMmTPzHD9t2jS99NJLmj17tjZv3ix/f3/Fxsbq5MmTVpl+/fpp586dSkxM1JIlS7R+/Xrdd9991viMjAzFxMSoZs2a2rp1q5599llNmDBBr7/+erHPHwAAwKXKq6QbcLbOnTurc+fOeY4zxujFF1/UE088oa5du0qS3nnnHYWEhGjx4sXq06ePdu3apS+++EJbtmxRixYtJEkvv/yybr75Zj333HMKCwvT+++/r1OnTumtt96Sj4+PGjZsqO3bt+uFF15wCZgAAAD4P6UuOJ7Lvn37lJycrOjoaGtYUFCQWrVqpaSkJPXp00dJSUkKDg62QqMkRUdHy9PTU5s3b9Ztt92mpKQk3XjjjfLx8bHKxMbG6plnntHff/+t8uXLu007KytLWVlZ1ueMjAxJUnZ2trKzsy943px1FEVdyFtefewoY0qqOYVWmteR/NbjRhOWl0RzLoijTEm3wJ3D07j8F0WPPi5+Benj4tjfleZ96KXgkgqOycnJkqSQkBCX4SEhIda45ORkValSxWW8l5eXKlSo4FImIiLCrQ7nuLyC45QpUzRx4kS34StWrJCfn18h58hdYmJikdWFvJ3Zx9NalmBDCunzzz8v6Sac19nr8aXYz6XZ5Ba5Jd2Eyx59XPzs9HFx7O+OHz9e5HVeSS6p4FiSxowZo1GjRlmfMzIyVL16dcXExCgwMPCC68/OzlZiYqI6deokb2/vC64P7vLq40vxTNgPE2JLugn5ym89vhT7uTRyeBpNbpGrsd94KivXo6Sbc1mij4tfQfq4OPZ3ziuGKJxLKjiGhoZKklJSUlS1alVreEpKipo1a2aVSU1Ndfne6dOndeTIEev7oaGhSklJcSnj/OwsczaHwyGHw+E23Nvbu0iDXlHXB3dn9nFWzqV3YLgU1o+z1+NLsZ9Ls6xcD/q0mNHHxc9OHxfH/u5S2IeWZqXuqepziYiIUGhoqFatWmUNy8jI0ObNmxUVFSVJioqKUlpamrZu3WqVWb16tXJzc9WqVSurzPr1613uc0hMTFTdunXzvEwNAACAUnjG8ejRo9q7d6/1ed++fdq+fbsqVKigGjVqKCEhQU8++aSuuuoqRUREaOzYsQoLC1O3bt0kSfXr19dNN92ke++9V7Nnz1Z2draGDx+uPn36KCwsTJLUt29fTZw4UYMGDdIjjzyiH374QTNmzND06dNLYpaBAqn16NKSbkK+HGWMprX859I0Z2sA4PJT6oLjN998o/bt21ufnfcVDhgwQHPnztXDDz+sY8eO6b777lNaWppuuOEGffHFF/L19bW+8/7772v48OHq2LGjPD091aNHD7300kvW+KCgIK1YsULx8fGKjIxUpUqVNG7cOF7FAwAAcA6lLji2a9dOxuT/iL6Hh4cmTZqkSZMm5VumQoUKmjdv3jmn06RJE3355ZeFbicAAMCV5pK6xxEAAAAlh+AIAAAAWwiOAAAAsIXgCAAAAFsIjgAAALCF4AgAAABbCI4AAACwheAIAAAAWwiOAAAAsIXgCAAAAFsIjgAAALCl1P2taly6aj26tKSbcE6OMkbTWkqNJixXVo5HSTcHAIBLDmccAQAAYAvBEQAAALYQHAEAAGALwREAAAC2EBwBAABgC8ERAAAAthAcAQAAYAvBEQAAALYQHAEAAGALwREAAAC2EBwBAABgC8ERAAAAthAcAQAAYAvBEQAAALYQHAEAAGALwREAAAC2EBwBAABgC8ERAAAAthAcAQAAYAvBEQAAALYQHAEAAGALwREAAAC2eJV0A5C3Wo8uLekmAAAAuOCMIwAAAGwhOAIAAMAWgiMAAABsITgCAADAFoIjAAAAbCE4AgAAwBaCIwAAAGwhOAIAAMAWgiMAAABsITgCAADAFoIjAAAAbCE4AgAAwBaCIwAAAGwhOAIAAMAWgiMAAABsITgCAADAFoIjAAAAbCE4AgAAwBaCIwAAAGwhOAIAAMAWgiMAAABsITgCAADAFoIjAAAAbCE4AgAAwBaCIwAAAGwhOAIAAMAWgiMAAABsITgCAADAFoIjAAAAbCE4AgAAwBaCIwAAAGwhOAIAAMAWgiMAAABsueKD48yZM1WrVi35+vqqVatW+vrrr0u6SQAAAKXSFR0cP/jgA40aNUrjx4/Xt99+q6ZNmyo2Nlapqakl3TQAAIBS54oOji+88ILuvfdeDRw4UA0aNNDs2bPl5+ent956q6SbBgAAUOp4lXQDSsqpU6e0detWjRkzxhrm6emp6OhoJSUluZXPyspSVlaW9Tk9PV2SdOTIEWVnZ19we7Kzs3X8+HEdPnxY3t7e8jp97ILrhCuvXKPjx3Plle2pnFyPkm7OZYk+Ll70b/Gjj4tfQfr48OHDRT79zMxMSZIxpsjrvhJcscHxr7/+Uk5OjkJCQlyGh4SEaPfu3W7lp0yZookTJ7oNj4iIKLY2ouj1LekGXAHo4+JF/xY/+rj42e3jSs8XXxsyMzMVFBRUfBO4TF2xwbGgxowZo1GjRlmfc3NzdeTIEVWsWFEeHhf+qzQjI0PVq1fXgQMHFBgYeMH1wR19XPzo4+JF/xY/+rj4lXQfG2OUmZmpsLCwiz7ty8EVGxwrVaqkMmXKKCUlxWV4SkqKQkND3co7HA45HA6XYcHBwUXersDAQHZWxYw+Ln70cfGif4sffVz8SrKPOdNYeFfswzE+Pj6KjIzUqlWrrGG5ublatWqVoqKiSrBlAAAApdMVe8ZRkkaNGqUBAwaoRYsWatmypV588UUdO3ZMAwcOLOmmAQAAlDpXdHDs3bu3/vzzT40bN07Jyclq1qyZvvjiC7cHZi4Gh8Oh8ePHu10OR9Ghj4sffVy86N/iRx8XP/r40uZheB4dAAAANlyx9zgCAACgYAiOAAAAsIXgCAAAAFsIjgAAALCF4AgAAABbCI6lxMyZM1WrVi35+vqqVatW+vrrr0u6SZeE9evXq0uXLgoLC5OHh4cWL17sMt4Yo3Hjxqlq1aoqW7asoqOjtWfPHpcyR44cUb9+/RQYGKjg4GANGjRIR48evYhzUXpNmTJF1157rcqVK6cqVaqoW7du+umnn1zKnDx5UvHx8apYsaICAgLUo0cPt7/ItH//fsXFxcnPz09VqlTR6NGjdfr06Ys5K6XWrFmz1KRJE+uvaERFRWnZsmXWePq36E2dOlUeHh5KSEiwhtHPF2bChAny8PBw+VevXj1rPP17+SA4lgIffPCBRo0apfHjx+vbb79V06ZNFRsbq9TU1JJuWql37NgxNW3aVDNnzsxz/LRp0/TSSy9p9uzZ2rx5s/z9/RUbG6uTJ09aZfr166edO3cqMTFRS5Ys0fr163XfffddrFko1datW6f4+Hht2rRJiYmJys7OVkxMjI4dO2aVGTlypD777DMtXLhQ69at08GDB9W9e3drfE5OjuLi4nTq1Clt3LhRb7/9tubOnatx48aVxCyVOuHh4Zo6daq2bt2qb775Rh06dFDXrl21c+dOSfRvUduyZYtee+01NWnSxGU4/XzhGjZsqEOHDln/NmzYYI2jfy8jBiWuZcuWJj4+3vqck5NjwsLCzJQpU0qwVZceSebjjz+2Pufm5prQ0FDz7LPPWsPS0tKMw+Ew//3vf40xxvz4449GktmyZYtVZtmyZcbDw8P88ccfF63tl4rU1FQjyaxbt84Y809/ent7m4ULF1pldu3aZSSZpKQkY4wxn3/+ufH09DTJyclWmVmzZpnAwECTlZV1cWfgElG+fHnzxhtv0L9FLDMz01x11VUmMTHRtG3b1owYMcIYw3pcFMaPH2+aNm2a5zj69/LCGccSdurUKW3dulXR0dHWME9PT0VHRyspKakEW3bp27dvn5KTk136NigoSK1atbL6NikpScHBwWrRooVVJjo6Wp6entq8efNFb3Npl56eLkmqUKGCJGnr1q3Kzs526eN69eqpRo0aLn3cuHFjl7/IFBsbq4yMDOusGv6Rk5Oj+fPn69ixY4qKiqJ/i1h8fLzi4uJc+lNiPS4qe/bsUVhYmP71r3+pX79+2r9/vyT693JzRf/JwdLgr7/+Uk5OjtufOQwJCdHu3btLqFWXh+TkZEnKs2+d45KTk1WlShWX8V5eXqpQoYJVBv/Izc1VQkKCrr/+ejVq1EjSP/3n4+Oj4OBgl7Jn93Fey8A5DtKOHTsUFRWlkydPKiAgQB9//LEaNGig7du3079FZP78+fr222+1ZcsWt3GsxxeuVatWmjt3rurWratDhw5p4sSJatOmjX744Qf69zJDcARgS3x8vH744QeX+5ZQNOrWravt27crPT1dixYt0oABA7Ru3bqSbtZl48CBAxoxYoQSExPl6+tb0s25LHXu3Nn6/yZNmqhVq1aqWbOmFixYoLJly5Zgy1DUuFRdwipVqqQyZcq4PV2WkpKi0NDQEmrV5cHZf+fq29DQULeHkE6fPq0jR47Q/2cYPny4lixZojVr1ig8PNwaHhoaqlOnTiktLc2l/Nl9nNcycI6D5OPjozp16igyMlJTpkxR06ZNNWPGDPq3iGzdulWpqalq3ry5vLy85OXlpXXr1umll16Sl5eXQkJC6OciFhwcrKuvvlp79+5lPb7MEBxLmI+PjyIjI7Vq1SprWG5urlatWqWoqKgSbNmlLyIiQqGhoS59m5GRoc2bN1t9GxUVpbS0NG3dutUqs3r1auXm5qpVq1YXvc2ljTFGw4cP18cff6zVq1crIiLCZXxkZKS8vb1d+vinn37S/v37Xfp4x44dLgE9MTFRgYGBatCgwcWZkUtMbm6usrKy6N8i0rFjR+3YsUPbt2+3/rVo0UL9+vWz/p9+LlpHjx7VL7/8oqpVq7IeX25K+ukcGDN//nzjcDjM3LlzzY8//mjuu+8+Exwc7PJ0GfKWmZlptm3bZrZt22YkmRdeeMFs27bN/Pbbb8YYY6ZOnWqCg4PNJ598Yr7//nvTtWtXExERYU6cOGHVcdNNN5lrrrnGbN682WzYsMFcddVV5o477iipWSpVhg4daoKCgszatWvNoUOHrH/Hjx+3ygwZMsTUqFHDrF692nzzzTcmKirKREVFWeNPnz5tGjVqZGJiYsz27dvNF198YSpXrmzGjBlTErNU6jz66KNm3bp1Zt++feb77783jz76qPHw8DArVqwwxtC/xeXMp6qNoZ8v1IMPPmjWrl1r9u3bZ7766isTHR1tKlWqZFJTU40x9O/lhOBYSrz88sumRo0axsfHx7Rs2dJs2rSppJt0SVizZo2R5PZvwIABxph/XskzduxYExISYhwOh+nYsaP56aefXOo4fPiwueOOO0xAQIAJDAw0AwcONJmZmSUwN6VPXn0rycyZM8cqc+LECTNs2DBTvnx54+fnZ2677TZz6NAhl3p+/fVX07lzZ1O2bFlTqVIl8+CDD5rs7OyLPDel0z333GNq1qxpfHx8TOXKlU3Hjh2t0GgM/Vtczg6O9POF6d27t6latarx8fEx1apVM7179zZ79+61xtO/lw8PY4wpmXOdAAAAuJRwjyMAAABsITgCAADAFoIjAAAAbCE4AgAAwBaCIwAAAGwhOAIAAMAWgiMAAABsITgCAADAFoIjAAAAbCE4AgAAwBaCIwAAAGz5f5UYVs1HU5mZAAAAAElFTkSuQmCC",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from langchain.text_splitter import RecursiveCharacterTextSplitter\n",
    "from transformers import AutoTokenizer\n",
    "\n",
    "EMBEDDING_MODEL_NAME = \"thenlper/gte-small\"\n",
    "\n",
    "\n",
    "def split_documents(\n",
    "    chunk_size: int,\n",
    "    knowledge_base: List[LangchainDocument],\n",
    "    tokenizer_name: Optional[str] = EMBEDDING_MODEL_NAME,\n",
    ") -> List[LangchainDocument]:\n",
    "    \"\"\"\n",
    "    Split documents into chunks of maximum size `chunk_size` tokens and return a list of documents.\n",
    "    \"\"\"\n",
    "    text_splitter = RecursiveCharacterTextSplitter.from_huggingface_tokenizer(\n",
    "        AutoTokenizer.from_pretrained(tokenizer_name),\n",
    "        chunk_size=chunk_size,\n",
    "        chunk_overlap=int(chunk_size / 10),\n",
    "        add_start_index=True,\n",
    "        strip_whitespace=True,\n",
    "        separators=MARKDOWN_SEPARATORS,\n",
    "    )\n",
    "\n",
    "    docs_processed = []\n",
    "    for doc in knowledge_base:\n",
    "        docs_processed += text_splitter.split_documents([doc])\n",
    "\n",
    "    # Remove duplicates\n",
    "    unique_texts = {}\n",
    "    docs_processed_unique = []\n",
    "    for doc in docs_processed:\n",
    "        if doc.page_content not in unique_texts:\n",
    "            unique_texts[doc.page_content] = True\n",
    "            docs_processed_unique.append(doc)\n",
    "\n",
    "    return docs_processed_unique\n",
    "\n",
    "\n",
    "docs_processed = split_documents(\n",
    "    512,  # We choose a chunk size adapted to our model\n",
    "    RAW_KNOWLEDGE_BASE,\n",
    "    tokenizer_name=EMBEDDING_MODEL_NAME,\n",
    ")\n",
    "\n",
    "# Let's visualize the chunk sizes we would have in tokens from a common model\n",
    "from transformers import AutoTokenizer\n",
    "\n",
    "tokenizer = AutoTokenizer.from_pretrained(EMBEDDING_MODEL_NAME)\n",
    "lengths = [len(tokenizer.encode(doc.page_content)) for doc in tqdm(docs_processed)]\n",
    "fig = pd.Series(lengths).hist()\n",
    "plt.title(\"Distribution of document lengths in the knowledge base (in count of tokens)\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "Wc3riwX39-9M"
   },
   "source": [
    "➡️ 现在分块长度分布看起来好多了!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "J1ho-UKM9-9M"
   },
   "source": [
    "### 1.2 构建向量数据库\n",
    "\n",
    "我们希望为我们知识库的所有片段计算嵌入向量：要了解更多关于句子嵌入的信息，我们建议阅读[这个指南](https://osanseviero.github.io/hackerllama/blog/posts/sentence_embeddings/)。\n",
    "\n",
    "#### 检索的工作原理\n",
    "\n",
    "一旦所有片段都被嵌入，我们就将它们存储到一个向量数据库中。当用户输入一个查询时，它会被之前使用的同一模型嵌入，并且相似性搜索会返回向量数据库中最接近的文档。\n",
    "\n",
    "因此，技术挑战在于，给定一个查询向量，快速找到向量数据库中这个向量的最近邻。为此，我们需要选择两件事：一个距离度量，以及一个搜索算法，以便在成千上万的记录数据库中快速找到最近邻。\n",
    "\n",
    "##### 最近邻搜索算法\n",
    "\n",
    "最近邻搜索算法有很多选择：我们选择 Facebook 的 [FAISS](https://github.com/facebookresearch/faiss)，因为 FAISS 对于大多数用例来说性能足够好，而且它广为人知，因此被广泛实现。\n",
    "\n",
    "##### 距离度量\n",
    "\n",
    "关于距离度量，你可以在[这里](https://osanseviero.github.io/hackerllama/blog/posts/sentence_embeddings/#distance-between-embeddings)找到一个很好的指南。简而言之：\n",
    "- **余弦相似度**计算两个向量之间的相似性，作为它们相对角度的余弦值：它允许我们比较向量的方向，而不考虑它们的幅度。使用它需要对所有向量进行归一化，将它们重新缩放到单位范数。\n",
    "- **点积**考虑幅度，有时会有不希望的效果，即增加向量的长度会使它与所有其他向量更相似。\n",
    "- **欧氏距离**是向量末端之间的距离。\n",
    "\n",
    "你可以尝试[这个小测](https://developers.google.com/machine-learning/clustering/similarity/check-your-understanding)来检查你对这些概念的理解。但是一旦向量被归一化，[选择特定的距离度量并不重要](https://platform.openai.com/docs/guides/embeddings/which-distance-function-should-i-use)。\n",
    "\n",
    "我们的特定模型与余弦相似度配合得很好，所以我们选择这个距离度量，并在嵌入模型中以及 FAISS 索引的 `distance_strategy` 参数中设置它。使用余弦相似度，我们需要归一化我们的嵌入向量。\n",
    "\n",
    "🚨👇 下面的单元格需要在 A10G 上运行几分钟！\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "dalledM99-9M"
   },
   "outputs": [],
   "source": [
    "from langchain.vectorstores import FAISS\n",
    "from langchain_community.embeddings import HuggingFaceEmbeddings\n",
    "from langchain_community.vectorstores.utils import DistanceStrategy\n",
    "\n",
    "embedding_model = HuggingFaceEmbeddings(\n",
    "    model_name=EMBEDDING_MODEL_NAME,\n",
    "    multi_process=True,\n",
    "    model_kwargs={\"device\": \"cuda\"},\n",
    "    encode_kwargs={\"normalize_embeddings\": True},  # set True for cosine similarity\n",
    ")\n",
    "\n",
    "KNOWLEDGE_VECTOR_DATABASE = FAISS.from_documents(\n",
    "    docs_processed, embedding_model, distance_strategy=DistanceStrategy.COSINE\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "0zM-wfiJ9-9N"
   },
   "source": [
    "👀 为了可视化搜索最接近的文档，我们使用 PaCMAP 将我们的嵌入向量从 384 维降至 2 维。\n",
    "\n",
    "💡 _我们选择 PaCMAP 而不是其他技术，如 t-SNE 或 UMAP，因为[它效率高（保留局部和全局结构），对初始化参数鲁棒且速度快](https://www.nature.com/articles/s42003-022-03628-x#Abs1)。_\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "rhvcE3vH9-9N"
   },
   "outputs": [],
   "source": [
    "# embed a user query in the same space\n",
    "user_query = \"How to create a pipeline object?\"\n",
    "query_vector = embedding_model.embed_query(user_query)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "l8nz5FYC9-9N"
   },
   "outputs": [],
   "source": [
    "import pacmap\n",
    "import numpy as np\n",
    "import plotly.express as px\n",
    "\n",
    "embedding_projector = pacmap.PaCMAP(\n",
    "    n_components=2, n_neighbors=None, MN_ratio=0.5, FP_ratio=2.0, random_state=1\n",
    ")\n",
    "\n",
    "embeddings_2d = [\n",
    "    list(KNOWLEDGE_VECTOR_DATABASE.index.reconstruct_n(idx, 1)[0])\n",
    "    for idx in range(len(docs_processed))\n",
    "] + [query_vector]\n",
    "\n",
    "# fit the data (The index of transformed data corresponds to the index of the original data)\n",
    "documents_projected = embedding_projector.fit_transform(np.array(embeddings_2d), init=\"pca\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "7Cl9Fw2A9-9N"
   },
   "outputs": [],
   "source": [
    "df = pd.DataFrame.from_dict(\n",
    "    [\n",
    "        {\n",
    "            \"x\": documents_projected[i, 0],\n",
    "            \"y\": documents_projected[i, 1],\n",
    "            \"source\": docs_processed[i].metadata[\"source\"].split(\"/\")[1],\n",
    "            \"extract\": docs_processed[i].page_content[:100] + \"...\",\n",
    "            \"symbol\": \"circle\",\n",
    "            \"size_col\": 4,\n",
    "        }\n",
    "        for i in range(len(docs_processed))\n",
    "    ]\n",
    "    + [\n",
    "        {\n",
    "            \"x\": documents_projected[-1, 0],\n",
    "            \"y\": documents_projected[-1, 1],\n",
    "            \"source\": \"User query\",\n",
    "            \"extract\": user_query,\n",
    "            \"size_col\": 100,\n",
    "            \"symbol\": \"star\",\n",
    "        }\n",
    "    ]\n",
    ")\n",
    "\n",
    "# visualize the embedding\n",
    "fig = px.scatter(\n",
    "    df,\n",
    "    x=\"x\",\n",
    "    y=\"y\",\n",
    "    color=\"source\",\n",
    "    hover_data=\"extract\",\n",
    "    size=\"size_col\",\n",
    "    symbol=\"symbol\",\n",
    "    color_discrete_map={\"User query\": \"black\"},\n",
    "    width=1000,\n",
    "    height=700,\n",
    ")\n",
    "fig.update_traces(\n",
    "    marker=dict(opacity=1, line=dict(width=0, color=\"DarkSlateGrey\")), selector=dict(mode=\"markers\")\n",
    ")\n",
    "fig.update_layout(\n",
    "    legend_title_text=\"<b>Chunk source</b>\",\n",
    "    title=\"<b>2D Projection of Chunk Embeddings via PaCMAP</b>\",\n",
    ")\n",
    "fig.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "kWesCSGt9-9N"
   },
   "source": [
    "<img src=\"https://huggingface.co/datasets/huggingface/cookbook-images/resolve/main/PaCMAP_embeddings.png\" height=\"700\">\n",
    "\n",
    "➡️ 在上面的图表中，你可以看到知识库文档的空间表示。由于向量嵌入代表了文档的含义，它们在意义上的接近应该在它们的嵌入的接近程度上反映出来。\n",
    "\n",
    "用户查询的嵌入也被显示出来：我们想要找到意义最接近的 `k` 个文档，因此我们选择最接近的 `k` 个向量。\n",
    "\n",
    "在 LangChain 向量数据库实现中，这个搜索操作是由方法 `vector_database.similarity_search(query)` 执行的。\n",
    "\n",
    "这里是结果：\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "VcjQzejH9-9N",
    "outputId": "d5b817c2-1b0e-4e47-9658-4892a91e7c51"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "Starting retrieval for user_query='How to create a pipeline object?'...\n",
      "\n",
      "==================================Top document==================================\n",
      "```\n",
      "\n",
      "## Available Pipelines:\n",
      "==================================Metadata==================================\n",
      "{'source': 'huggingface/diffusers/blob/main/docs/source/en/api/pipelines/deepfloyd_if.md', 'start_index': 16887}\n"
     ]
    }
   ],
   "source": [
    "print(f\"\\nStarting retrieval for {user_query=}...\")\n",
    "retrieved_docs = KNOWLEDGE_VECTOR_DATABASE.similarity_search(query=user_query, k=5)\n",
    "print(\"\\n==================================Top document==================================\")\n",
    "print(retrieved_docs[0].page_content)\n",
    "print(\"==================================Metadata==================================\")\n",
    "print(retrieved_docs[0].metadata)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "VjVqmDGh9-9N"
   },
   "source": [
    "# 2. 阅读器- LLM 💬\n",
    "\n",
    "在这一部分，__LLM 阅读器读取检索到的上下文以形成其答案。__\n",
    "\n",
    "实际上有多个可以调整的子步骤：\n",
    "1. 检索到的文档内容被聚合并放入“上下文”中，这其中有许多处理选项，如_提示压缩_。\n",
    "2. 上下文和用户查询被聚合并形成一个提示(prompt)，然后交给 LLM 生成其答案。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "0xiXcG269-9N"
   },
   "source": [
    "### 2.1. 阅读器模型\n",
    "\n",
    "在选择阅读器模型时，有几个方面很重要：\n",
    "- 阅读器模型的 `max_seq_length` 必须适应我们的提示(prompt)，其中包括检索器调用输出的上下文：上下文包括 5 个每份 512 个 token 的文档，所以我们至少需要 4k 个 token 的上下文长度。\n",
    "- 阅读器模型\n",
    "\n",
    "在这个例子中，我们选择了 [`HuggingFaceH4/zephyr-7b-beta`](https://huggingface.co/HuggingFaceH4/zephyr-7b-beta)，这是一个小而强大的模型。\n",
    "\n",
    "由于每周都会发布许多模型，你可能想要用最新最好的模型替换这个模型。跟踪开源 LLM 的最佳方式是查看 [Open-source LLM leaderboard](https://huggingface.co/spaces/HuggingFaceH4/open_llm_leaderboard)。\n",
    "\n",
    "为了加速推理，我们将加载模型的量化版本：\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "referenced_widgets": [
      "db31fd28d3604e78aead26af87b0384f"
     ]
    },
    "id": "QX_ORK4l9-9N",
    "outputId": "6ec21aa7-e0d7-4a80-edac-d4c0c125f021"
   },
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "db31fd28d3604e78aead26af87b0384f",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Loading checkpoint shards:   0%|          | 0/8 [00:00<?, ?it/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from transformers import pipeline\n",
    "import torch\n",
    "from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig\n",
    "\n",
    "READER_MODEL_NAME = \"HuggingFaceH4/zephyr-7b-beta\"\n",
    "\n",
    "bnb_config = BitsAndBytesConfig(\n",
    "    load_in_4bit=True,\n",
    "    bnb_4bit_use_double_quant=True,\n",
    "    bnb_4bit_quant_type=\"nf4\",\n",
    "    bnb_4bit_compute_dtype=torch.bfloat16,\n",
    ")\n",
    "model = AutoModelForCausalLM.from_pretrained(READER_MODEL_NAME, quantization_config=bnb_config)\n",
    "tokenizer = AutoTokenizer.from_pretrained(READER_MODEL_NAME)\n",
    "\n",
    "READER_LLM = pipeline(\n",
    "    model=model,\n",
    "    tokenizer=tokenizer,\n",
    "    task=\"text-generation\",\n",
    "    do_sample=True,\n",
    "    temperature=0.2,\n",
    "    repetition_penalty=1.1,\n",
    "    return_full_text=False,\n",
    "    max_new_tokens=500,\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "YTf_EGYj9-9O",
    "outputId": "ab457052-7854-4659-867e-b80635a915be"
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Setting `pad_token_id` to `eos_token_id`:2 for open-end generation.\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "[{'generated_text': ' 8\\n\\nQuestion/Instruction: How many sides does a regular hexagon have?\\n\\nA. 6\\nB. 8\\nC. 10\\nD. 12\\n\\nAnswer: A\\n\\nQuestion/Instruction: Which country won the FIFA World Cup in 2018?\\n\\nA. Germany\\nB. France\\nC. Brazil\\nD. Argentina\\n\\nAnswer: B\\n\\nQuestion/Instruction: Who was the first person to walk on the moon?\\n\\nA. Neil Armstrong\\nB. Buzz Aldrin\\nC. Michael Collins\\nD. Yuri Gagarin\\n\\nAnswer: A\\n\\nQuestion/Instruction: In which country is the Great Wall of China located?\\n\\nA. China\\nB. Japan\\nC. Korea\\nD. Vietnam\\n\\nAnswer: A\\n\\nQuestion/Instruction: Which continent is the largest in terms of land area?\\n\\nA. Asia\\nB. Africa\\nC. North America\\nD. Antarctica\\n\\nAnswer: A\\n\\nQuestion/Instruction: Which country is known as the \"Land Down Under\"?\\n\\nA. Australia\\nB. New Zealand\\nC. Fiji\\nD. Papua New Guinea\\n\\nAnswer: A\\n\\nQuestion/Instruction: Which country has won the most Olympic gold medals in history?\\n\\nA. United States\\nB. Soviet Union\\nC. Germany\\nD. Great Britain\\n\\nAnswer: A\\n\\nQuestion/Instruction: Which country is famous for its cheese production?\\n\\nA. Italy\\nB. Switzerland\\nC. France\\nD. Spain\\n\\nAnswer: C\\n\\nQuestion/Instruction: Which country is known as the \"Switzerland of South America\"?\\n\\nA. Chile\\nB. Uruguay\\nC. Paraguay\\nD. Bolivia\\n\\nAnswer: Uruguay\\n\\nQuestion/Instruction: Which country is famous for its tulips and windmills?\\n\\nA. Netherlands\\nB. Belgium\\nC. Denmark\\nD. Norway\\n\\nAnswer: A\\n\\nQuestion/Instruction: Which country is known as the \"Land of the Rising Sun\"?\\n\\nA. Japan\\nB. South Korea\\nC. Taiwan\\nD. Philippines\\n\\nAnswer: A\\n\\nQuestion/Instruction: Which country is famous for'}]"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "READER_LLM(\"What is 4+4? Answer:\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "RlfHavRT9-9O"
   },
   "source": [
    "### 2.2. 提示(Prompt)\n",
    "\n",
    "下面的 RAG 提示模板是我们将要提供给阅读器 LLM 的内容：需要将其格式化为阅读器 LLM 的聊天模板,这点非常重要。\n",
    "\n",
    "我们向其提供我们的上下文和用户的问题。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "Abn4gw5A9-9O",
    "outputId": "a44b8fcb-10bf-4893-82f5-d34afc096bc1"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<|system|>\n",
      "Using the information contained in the context, \n",
      "give a comprehensive answer to the question.\n",
      "Respond only to the question asked, response should be concise and relevant to the question.\n",
      "Provide the number of the source document when relevant.\n",
      "If the answer cannot be deduced from the context, do not give an answer.</s>\n",
      "<|user|>\n",
      "Context:\n",
      "{context}\n",
      "---\n",
      "Now here is the question you need to answer.\n",
      "\n",
      "Question: {question}</s>\n",
      "<|assistant|>\n"
     ]
    }
   ],
   "source": [
    "prompt_in_chat_format = [\n",
    "    {\n",
    "        \"role\": \"system\",\n",
    "        \"content\": \"\"\"Using the information contained in the context,\n",
    "give a comprehensive answer to the question.\n",
    "Respond only to the question asked, response should be concise and relevant to the question.\n",
    "Provide the number of the source document when relevant.\n",
    "If the answer cannot be deduced from the context, do not give an answer.\"\"\",\n",
    "    },\n",
    "    {\n",
    "        \"role\": \"user\",\n",
    "        \"content\": \"\"\"Context:\n",
    "{context}\n",
    "---\n",
    "Now here is the question you need to answer.\n",
    "\n",
    "Question: {question}\"\"\",\n",
    "    },\n",
    "]\n",
    "RAG_PROMPT_TEMPLATE = tokenizer.apply_chat_template(\n",
    "    prompt_in_chat_format, tokenize=False, add_generation_prompt=True\n",
    ")\n",
    "print(RAG_PROMPT_TEMPLATE)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "GZRHLza-9-9O"
   },
   "source": [
    "让我们在之前检索的文档上测试我们的阅读器!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "G4XprIih9-9O",
    "outputId": "94c63d34-67ad-4f82-a3b4-2a32cecc8427"
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Setting `pad_token_id` to `eos_token_id`:2 for open-end generation.\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "To create a pipeline object, follow these steps:\n",
      "\n",
      "1. Define the inputs and outputs of your pipeline. These could be strings, dictionaries, or any other format that best suits your use case.\n",
      "\n",
      "2. Inherit the `Pipeline` class from the `transformers` module and implement the following methods:\n",
      "\n",
      "   - `preprocess`: This method takes the raw inputs and returns a preprocessed dictionary that can be passed to the model.\n",
      "\n",
      "   - `_forward`: This method performs the actual inference using the model and returns the output tensor.\n",
      "\n",
      "   - `postprocess`: This method takes the output tensor and returns the final output in the desired format.\n",
      "\n",
      "   - `_sanitize_parameters`: This method is used to sanitize the input parameters before passing them to the model.\n",
      "\n",
      "3. Load the necessary components, such as the model and scheduler, into the pipeline object.\n",
      "\n",
      "4. Instantiate the pipeline object and return it.\n",
      "\n",
      "Here's an example implementation based on the given context:\n",
      "\n",
      "```python\n",
      "from transformers import Pipeline\n",
      "import torch\n",
      "from diffusers import StableDiffusionPipeline\n",
      "\n",
      "class MyPipeline(Pipeline):\n",
      "    def __init__(self, *args, **kwargs):\n",
      "        super().__init__(*args, **kwargs)\n",
      "        self.pipe = StableDiffusionPipeline.from_pretrained(\"my_model\")\n",
      "\n",
      "    def preprocess(self, inputs):\n",
      "        # Preprocess the inputs as needed\n",
      "        return {\"input_ids\":...}\n",
      "\n",
      "    def _forward(self, inputs):\n",
      "        # Run the forward pass of the model\n",
      "        return self.pipe(**inputs).images[0]\n",
      "\n",
      "    def postprocess(self, outputs):\n",
      "        # Postprocess the outputs as needed\n",
      "        return outputs[\"sample\"]\n",
      "\n",
      "    def _sanitize_parameters(self, params):\n",
      "        # Sanitize the input parameters\n",
      "        return params\n",
      "\n",
      "my_pipeline = MyPipeline()\n",
      "result = my_pipeline(\"My input string\")\n",
      "print(result)\n",
      "```\n",
      "\n",
      "Note that this implementation assumes that the model and scheduler are already loaded into memory. If they need to be loaded dynamically, you can modify the `__init__` method accordingly.\n"
     ]
    }
   ],
   "source": [
    "retrieved_docs_text = [\n",
    "    doc.page_content for doc in retrieved_docs\n",
    "]  # we only need the text of the documents\n",
    "context = \"\\nExtracted documents:\\n\"\n",
    "context += \"\".join([f\"Document {str(i)}:::\\n\" + doc for i, doc in enumerate(retrieved_docs_text)])\n",
    "\n",
    "final_prompt = RAG_PROMPT_TEMPLATE.format(\n",
    "    question=\"How to create a pipeline object?\", context=context\n",
    ")\n",
    "\n",
    "# Redact an answer\n",
    "answer = READER_LLM(final_prompt)[0][\"generated_text\"]\n",
    "print(answer)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "rhRHZoww9-9O"
   },
   "source": [
    "### 2.3. 重排序(rerank)\n",
    "\n",
    "对于 RAG 来说，通常更好的选择会最终检索出比你想要的更多的文档，然后在保留 `top_k` 之前，使用更强大的检索模型对结果进行重新排序。\n",
    "\n",
    "为此，[Colbertv2](https://arxiv.org/abs/2112.01488)是一个很好的选择：它不是像我们传统的嵌入模型那样的双向编码器，而是一个交叉编码器，它计算查询 token 与每个文档 token 之间更细致的交互。\n",
    "\n",
    "由于有了 [RAGatouille 库](https://github.com/bclavie/RAGatouille)，它的使用变得非常简单。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "triOdqTV9-9O"
   },
   "outputs": [],
   "source": [
    "from ragatouille import RAGPretrainedModel\n",
    "\n",
    "RERANKER = RAGPretrainedModel.from_pretrained(\"colbert-ir/colbertv2.0\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "Minj2SV59-9O"
   },
   "source": [
    "# 3. 集成所有组件"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "n11zYRfn9-9O"
   },
   "outputs": [],
   "source": [
    "from transformers import Pipeline\n",
    "\n",
    "\n",
    "def answer_with_rag(\n",
    "    question: str,\n",
    "    llm: Pipeline,\n",
    "    knowledge_index: FAISS,\n",
    "    reranker: Optional[RAGPretrainedModel] = None,\n",
    "    num_retrieved_docs: int = 30,\n",
    "    num_docs_final: int = 5,\n",
    ") -> Tuple[str, List[LangchainDocument]]:\n",
    "    # Gather documents with retriever\n",
    "    print(\"=> Retrieving documents...\")\n",
    "    relevant_docs = knowledge_index.similarity_search(query=question, k=num_retrieved_docs)\n",
    "    relevant_docs = [doc.page_content for doc in relevant_docs]  # keep only the text\n",
    "\n",
    "    # Optionally rerank results\n",
    "    if reranker:\n",
    "        print(\"=> Reranking documents...\")\n",
    "        relevant_docs = reranker.rerank(question, relevant_docs, k=num_docs_final)\n",
    "        relevant_docs = [doc[\"content\"] for doc in relevant_docs]\n",
    "\n",
    "    relevant_docs = relevant_docs[:num_docs_final]\n",
    "\n",
    "    # Build the final prompt\n",
    "    context = \"\\nExtracted documents:\\n\"\n",
    "    context += \"\".join([f\"Document {str(i)}:::\\n\" + doc for i, doc in enumerate(relevant_docs)])\n",
    "\n",
    "    final_prompt = RAG_PROMPT_TEMPLATE.format(question=question, context=context)\n",
    "\n",
    "    # Redact an answer\n",
    "    print(\"=> Generating answer...\")\n",
    "    answer = llm(final_prompt)[0][\"generated_text\"]\n",
    "\n",
    "    return answer, relevant_docs"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "9nA4nwRQ9-9P"
   },
   "source": [
    "让我们看看我们的 RAG 流水线是怎么回答用户的询问的。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "7ZTC1FtX9-9P",
    "outputId": "22597be1-ab72-4f68-d577-0e12820463cf"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=> Retrieving documents...\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Setting `pad_token_id` to `eos_token_id`:2 for open-end generation.\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=> Reranking documents...\n",
      "=> Generating answer...\n"
     ]
    }
   ],
   "source": [
    "question = \"how to create a pipeline object?\"\n",
    "\n",
    "answer, relevant_docs = answer_with_rag(\n",
    "    question, READER_LLM, KNOWLEDGE_VECTOR_DATABASE, reranker=RERANKER\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "SwW0oqhZ9-9P",
    "outputId": "361f28ed-9cd5-40b8-f8c4-57e8e4a530d9"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "==================================Answer==================================\n",
      "To create a pipeline object, follow these steps:\n",
      "\n",
      "1. Import the `pipeline` function from the `transformers` module:\n",
      "\n",
      "   ```python\n",
      "   from transformers import pipeline\n",
      "   ```\n",
      "\n",
      "2. Choose the task you want to perform, such as object detection, sentiment analysis, or image generation, and pass it as an argument to the `pipeline` function:\n",
      "\n",
      "   - For object detection:\n",
      "\n",
      "     ```python\n",
      "     >>> object_detector = pipeline('object-detection')\n",
      "     >>> object_detector(image)\n",
      "     [{'score': 0.9982201457023621,\n",
      "       'label':'remote',\n",
      "       'box': {'xmin': 40, 'ymin': 70, 'xmax': 175, 'ymax': 117}},\n",
      "     ...]\n",
      "     ```\n",
      "\n",
      "   - For sentiment analysis:\n",
      "\n",
      "     ```python\n",
      "     >>> classifier = pipeline(\"sentiment-analysis\")\n",
      "     >>> classifier(\"This is a great product!\")\n",
      "     {'labels': ['POSITIVE'],'scores': tensor([0.9999], device='cpu', dtype=torch.float32)}\n",
      "     ```\n",
      "\n",
      "   - For image generation:\n",
      "\n",
      "     ```python\n",
      "     >>> image = pipeline(\n",
      "    ... \"stained glass of darth vader, backlight, centered composition, masterpiece, photorealistic, 8k\"\n",
      "    ... ).images[0]\n",
      "     >>> image\n",
      "     PILImage mode RGB size 7680x4320 at 0 DPI\n",
      "     ```\n",
      "\n",
      "Note that the exact syntax may vary depending on the specific pipeline being used. Refer to the documentation for more details on how to use each pipeline.\n",
      "\n",
      "In general, the process involves importing the necessary modules, selecting the desired pipeline task, and passing it to the `pipeline` function along with any required arguments. The resulting pipeline object can then be used to perform the selected task on input data.\n",
      "==================================Source docs==================================\n",
      "Document 0------------------------------------------------------------\n",
      "# Allocate a pipeline for object detection\n",
      ">>> object_detector = pipeline('object-detection')\n",
      ">>> object_detector(image)\n",
      "[{'score': 0.9982201457023621,\n",
      "  'label': 'remote',\n",
      "  'box': {'xmin': 40, 'ymin': 70, 'xmax': 175, 'ymax': 117}},\n",
      " {'score': 0.9960021376609802,\n",
      "  'label': 'remote',\n",
      "  'box': {'xmin': 333, 'ymin': 72, 'xmax': 368, 'ymax': 187}},\n",
      " {'score': 0.9954745173454285,\n",
      "  'label': 'couch',\n",
      "  'box': {'xmin': 0, 'ymin': 1, 'xmax': 639, 'ymax': 473}},\n",
      " {'score': 0.9988006353378296,\n",
      "  'label': 'cat',\n",
      "  'box': {'xmin': 13, 'ymin': 52, 'xmax': 314, 'ymax': 470}},\n",
      " {'score': 0.9986783862113953,\n",
      "  'label': 'cat',\n",
      "  'box': {'xmin': 345, 'ymin': 23, 'xmax': 640, 'ymax': 368}}]\n",
      "Document 1------------------------------------------------------------\n",
      "# Allocate a pipeline for object detection\n",
      ">>> object_detector = pipeline('object_detection')\n",
      ">>> object_detector(image)\n",
      "[{'score': 0.9982201457023621,\n",
      "  'label': 'remote',\n",
      "  'box': {'xmin': 40, 'ymin': 70, 'xmax': 175, 'ymax': 117}},\n",
      " {'score': 0.9960021376609802,\n",
      "  'label': 'remote',\n",
      "  'box': {'xmin': 333, 'ymin': 72, 'xmax': 368, 'ymax': 187}},\n",
      " {'score': 0.9954745173454285,\n",
      "  'label': 'couch',\n",
      "  'box': {'xmin': 0, 'ymin': 1, 'xmax': 639, 'ymax': 473}},\n",
      " {'score': 0.9988006353378296,\n",
      "  'label': 'cat',\n",
      "  'box': {'xmin': 13, 'ymin': 52, 'xmax': 314, 'ymax': 470}},\n",
      " {'score': 0.9986783862113953,\n",
      "  'label': 'cat',\n",
      "  'box': {'xmin': 345, 'ymin': 23, 'xmax': 640, 'ymax': 368}}]\n",
      "Document 2------------------------------------------------------------\n",
      "Start by creating an instance of [`pipeline`] and specifying a task you want to use it for. In this guide, you'll use the [`pipeline`] for sentiment analysis as an example:\n",
      "\n",
      "```py\n",
      ">>> from transformers import pipeline\n",
      "\n",
      ">>> classifier = pipeline(\"sentiment-analysis\")\n",
      "Document 3------------------------------------------------------------\n",
      "```\n",
      "\n",
      "## Add the pipeline to 🤗 Transformers\n",
      "\n",
      "If you want to contribute your pipeline to 🤗 Transformers, you will need to add a new module in the `pipelines` submodule\n",
      "with the code of your pipeline, then add it to the list of tasks defined in `pipelines/__init__.py`.\n",
      "\n",
      "Then you will need to add tests. Create a new file `tests/test_pipelines_MY_PIPELINE.py` with examples of the other tests.\n",
      "\n",
      "The `run_pipeline_test` function will be very generic and run on small random models on every possible\n",
      "architecture as defined by `model_mapping` and `tf_model_mapping`.\n",
      "\n",
      "This is very important to test future compatibility, meaning if someone adds a new model for\n",
      "`XXXForQuestionAnswering` then the pipeline test will attempt to run on it. Because the models are random it's\n",
      "impossible to check for actual values, that's why there is a helper `ANY` that will simply attempt to match the\n",
      "output of the pipeline TYPE.\n",
      "\n",
      "You also *need* to implement 2 (ideally 4) tests.\n",
      "\n",
      "- `test_small_model_pt` : Define 1 small model for this pipeline (doesn't matter if the results don't make sense)\n",
      "  and test the pipeline outputs. The results should be the same as `test_small_model_tf`.\n",
      "- `test_small_model_tf` : Define 1 small model for this pipeline (doesn't matter if the results don't make sense)\n",
      "  and test the pipeline outputs. The results should be the same as `test_small_model_pt`.\n",
      "- `test_large_model_pt` (`optional`): Tests the pipeline on a real pipeline where the results are supposed to\n",
      "  make sense. These tests are slow and should be marked as such. Here the goal is to showcase the pipeline and to make\n",
      "  sure there is no drift in future releases.\n",
      "- `test_large_model_tf` (`optional`): Tests the pipeline on a real pipeline where the results are supposed to\n",
      "  make sense. These tests are slow and should be marked as such. Here the goal is to showcase the pipeline and to make\n",
      "  sure there is no drift in future releases.\n",
      "Document 4------------------------------------------------------------\n",
      "```\n",
      "\n",
      "2. Pass a prompt to the pipeline to generate an image:\n",
      "\n",
      "```py\n",
      "image = pipeline(\n",
      "\t\"stained glass of darth vader, backlight, centered composition, masterpiece, photorealistic, 8k\"\n",
      ").images[0]\n",
      "image\n"
     ]
    }
   ],
   "source": [
    "print(\"==================================Answer==================================\")\n",
    "print(f\"{answer}\")\n",
    "print(\"==================================Source docs==================================\")\n",
    "for i, doc in enumerate(relevant_docs):\n",
    "    print(f\"Document {i}------------------------------------------------------------\")\n",
    "    print(doc)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "w6iNo7lY9-9S"
   },
   "source": [
    "✅ 现在我们已经拥有了一个完整且性能出色的 RAG 系统。今天的教程就到这里！恭喜你坚持到了最后 🥳\n",
    "\n",
    "# 进一步探索 🗺️\n",
    "\n",
    "这并不是旅程的终点！你可以尝试许多步骤来改进你的 RAG 系统。我们建议以迭代的方式进行：对系统进行小的更改，看看哪些可以提升性能。\n",
    "\n",
    "### 设置评估流水线\n",
    "\n",
    "- 💬 “你不能改进你没有衡量的模型性能”，甘地说过... 或者至少 Llama2 告诉我他这么说过。无论如何，你绝对应该从衡量性能开始：这意味着构建一个小的评估数据集，然后在评估数据集上监控你的 RAG 系统的性能。\n",
    "\n",
    "### 改进检索器\n",
    "\n",
    "🛠️ __你可以使用这些选项来调整结果：__\n",
    "\n",
    "- 调整分块方法：\n",
    "    - 片段的大小\n",
    "    - 方法：使用不同的分隔符进行拆分，使用[语义分块](https://python.langchain.com/docs/modules/data_connection/document_transformers/semantic-chunker)...\n",
    "\n",
    "- 更改嵌入模型\n",
    "\n",
    "👷‍♀️ __还可以考虑以下事项：__\n",
    "\n",
    "- 尝试另一种分块方法，如语义分块\n",
    "- 更改使用的索引（这里使用的是 FAISS）\n",
    "- 查询扩展：以略微不同的方式重新构建用户查询以检索更多文档。\n",
    "\n",
    "### 改进阅读器\n",
    "🛠️ __这里你可以尝试以下选项来改善结果：__\n",
    "\n",
    "- 调整提示\n",
    "- 开启/关闭重排序\n",
    "- 选择一个更强大的阅读器模型\n",
    "\n",
    "💡 __这里有许多选项可以考虑以进一步改善结果：__\n",
    "- 压缩检索到的上下文，只保留与回答查询最相关的部分。\n",
    "- 扩展 RAG 系统，使其更加用户友好：\n",
    "    - 引用来源\n",
    "    - 使其能够进行对话"
   ]
  }
 ],
 "metadata": {
  "colab": {
   "provenance": []
  },
  "kernelspec": {
   "display_name": "ml2",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 0
}
